How can I use enum to Binding data in SwiftUI? - enums

I'm trying to bind to an enum instance's rawValue
enum Brand: Int, CaseIterable, Codable {
case toyota = 0
case mazda
case suzuki
}
struct Car {
var brand: Brand
}
struct CarView: View {
#Binding car: Car
var body: some View {
SomeView(selectionInt: $car.brand)
}
}
But I get this error:
Cannot convert value of type 'Binding' to expected argument
type 'Binding'.
If've tried using $car.brand.rawValue instead (in SomeView's parameters) but then I get this error:
Cannot assign to property: 'rawValue' is immutable.
How can i bind my View to the model instance's rawValue?

Using a brandProxy to maintain the integrity of the other Car variables
import SwiftUI
enum Brand: Int, CaseIterable, Codable {
case toyota = 0
case mazda
case suzuki
case unknown
func description() -> String {
var result = "unknown"
switch self {
case .toyota:
result = "toyota"
case .mazda:
result = "mazda"
case .suzuki:
result = "suzuki"
case .unknown:
result = "unknown"
}
return result
}
}
struct CarView: View {
//This makes it mutable
#State var car: Car
var brandProxy: Binding<Int> {
Binding<Int>(
get: {
car.brand.rawValue
},
set: {
car.brand = Brand(rawValue: $0) ?? Brand.unknown
}
)
}
var body: some View {
VStack{
Text(car.brand.description())
SomeView(selectedIndex: brandProxy)
}
}
}

For this kind of binding you need a custom Binding to help SwiftUI to Bind data!
Info: You can not change rawValue of enum because they are immutable!
and also try of changing them in way of Binding would not help you, they are immutable! you can get them not set them!
So with that info you can know why SwiftUI does not want Bind it at fist place! because if SwiftUI make that Binding possible that would be a BIG violation of enum rawValues! then we will try use Binding power in this way that we change the Int of car not Enum. see the code for example.
import SwiftUI
struct ContentView: View {
#State private var selectedCar: Car = Car(brand: Brand.suzuki)
var body: some View {
VStack {
Picker("", selection: $selectedCar.brand) {
ForEach(Brand.allCases, id: \.self) { car in
Text(car.description)
}
}
.pickerStyle(SegmentedPickerStyle())
}
.padding()
CarView(selectionInt: Binding.init(get: { () -> Int in return selectedCar.brand.rawValue },
set: { newValue in
if let unwrappedBrand = Brand(rawValue: newValue) { selectedCar = Car(brand: unwrappedBrand) } }) )
}
}
struct CarView: View {
#Binding var selectionInt: Int
var body: some View {
Text("selected Int: " + selectionInt.description)
.padding()
let selectedCar = (Brand(rawValue: selectionInt)?.description ?? "unknown")
Text( "selected car: " + selectedCar)
.padding()
VStack(spacing: 20.0) {
Button("select toyota") { selectionInt = 0 }
Button("select mazda") { selectionInt = 1 }
Button("select suzuki") { selectionInt = 2 }
}
.font(Font.body.weight(Font.Weight.bold))
}
}
enum Brand: Int, CaseIterable, Codable, CustomStringConvertible {
case toyota = 0
case mazda = 1
case suzuki = 2
var description: String {
switch self {
case .toyota: return "toyota"
case .mazda: return "mazda"
case .suzuki: return "suzuki"
}
}
}
struct Car {
var brand: Brand
}

Where appropriate, I like defining my enums to be String-based. This is especially useful when the data will be saved to Firebase. When I look at the database, I see the actual values instead of 0s, 1s and 2s that I then have to remember/lookup what they actually stand for.
enum Brand: String, CaseIterable, Codable {
case toyota = "Toyota"
case mazda = "Mazda"
case suzuki = "Suzuki"
}
struct Car {
var brand: Brand
}
struct CarView: View {
#State car: Car
var body: some View {
SomeView(with: $car.brand)
}
}
struct SomeView: some View {
#Binding var brand: Brand
...
}
#State makes it mutable in the original View. #Binding allows another view to make changes and propagate them back to the original view. Going back to the OP's original question, it seems that where you say #Binding should have been #State and you should have a #Binding definition in the SomeView definition. So, you were almost there.

Related

Swiftui TextEditor seems not to respond to state changes

I am having trouble getting the SwiftUI TextEditor to work when it is in a Child View.
This is a small example that demonstrates the issue for me:
import SwiftUI
struct ContentView: View {
#State private var someText: String = "Hello World"
var body: some View {
VStack {
HStack {
Button("Text 1", action: {someText = "hello"})
Button("Text 2", action: {someText = "world"})
}
ViewWithEditor(entry: $someText)
}
}
}
struct ViewWithEditor: View {
#Binding var entry: String
#State private var localString: String
var body: some View
{
VStack {
TextEditor(text: $localString)
}
}
init(entry: Binding<String>) {
self._entry = entry
self._localString = State(initialValue: entry.wrappedValue)
print("init set local String to: \(localString)")
}
}
When I click the buttons I expected the Editor text to change, however it remains with its initial value.
The print statement show that the "localString" variable is being updated.
Is TextEditor broken or am I missing something fundamental ??
If you move the buttons into the same view as the TextEditor, directly changing local state var it works as expected.
This is being run under MacOS in case it makes a difference.
TIA Alan.
Ok. So a proxy Binding does the job for me. See updated editor view below:
struct ViewWithEditor: View {
#Binding var entry: String
var body: some View
{
let localString = Binding<String> (
get: {
entry
},
set: {
entry = $0
}
)
return VStack {
Text(entry)
TextEditor(text: localString)
}
}
A bit more ugly (proxy bindings just seem clutter), but in some ways simpler..
It allows for the result of the edit to be reviewed / rejected before being pushed into the bound var.
This is occurring because the binding entry var is not actually being used after initialization of ViewWithEditor. In order to make this work without using the proxy binding add onChange to the ViewWithEditor as below:
struct ViewWithEditor: View {
#Binding var entry: String
#State private var localString: String
var body: some View
{
VStack {
TextEditor(text: $localString)
}
.onChange(of: entry) {
localString = $0
}
}
init(entry: Binding<String>) {
self._entry = entry
self._localString = State(initialValue: entry.wrappedValue)
print("init set local String to: \(localString)")
}
}
The problem here is that now entry is not updating if localString changes. One could just use the same approach as before:
var body: some View
{
VStack {
TextEditor(text: $localString)
}
.onChange(of: entry) {
localString = $0
}
.onChange(of: localString) {
entry = $0
}
}
but why not just use $entry as the binding string for TextEditor?

SwiftUI - Dynamic NSPredicate If Statement

How can I create a predicate so that when the user selects "Full Body" it returns the entire list with no predicate? Right now, it is returning "part" which corresponds to the muscle groups I have set (Abs, Legs, Push, Pull). I want to return all of the options when "Full Body" is selected. How could I write an If statement so that the predicate is not used?
import SwiftUI
var parts = ["Abs", "Legs", "Push", "Pull", "Full Body"]
struct ExerciseList: View {
#State private var selectedPart = " "
var body: some View {
NavigationView {
VStack (alignment: .leading) {
NavigationLink(destination: AddExerciseView()){
Text("Add Exercise")
.fontWeight(.bold)
}
Picker("Body Part", selection: $selectedPart) {
ForEach(parts, id:\.self) { part in
Text(part)
}
}.pickerStyle(.segmented)
ListView(part:selectedPart)
}
}
}
}
import SwiftUI
struct ListView: View {
var part: String
#FetchRequest var exercises: FetchedResults<Exercise>
init(part: String) {
self.part = part
self._exercises = FetchRequest(
entity: Exercise.entity(),
sortDescriptors: [],
predicate: NSPredicate(format: "musclegroup == %#", part as any CVarArg)
)
}
var body: some View {
List(exercises) { e in
Text(e.exercisename)
}
}
}
It's not a good idea to init objects inside View structs because the heap allocation slows things down. You could either have all the predicates created before hand or create one when the picker value changes, e.g. something like this:
// all the Picker samples in the docs tend to use enums.
enum Part: String, Identifiable, CaseIterable {
case abs
case legs
case push
case pull
case fullBody
var id: Self { self }
// Apple sometimes does it like this
// var localizedName: LocalizedStringKey {
// switch self {
// case .abs: return "Abs"
// case .legs: return "Legs"
// case .push: return "Push"
// case .pull: return "Pull"
// case .fullBody: return "Full Body"
// }
// }
}
struct ExerciseListConfig {
var selectedPart: Part = .fullBody {
didSet {
if selectedPart == .fullBody {
predicate = nil
}
else {
// note this will use the lower case string
predicate = NSPredicate(format: "musclegroup == %#", selectedPart.rawValue)
}
}
}
var predicate: NSPredicate?
}
struct ExerciseList: View {
#State private var config = ExerciseListConfig()
var body: some View {
NavigationView {
VStack (alignment: .leading) {
Picker("Body Part", selection: $config.selectedPart) {
//ForEach(Part.allCases) // Apple sometimes does this but means you can't easily change the display order.
Text("Abs").tag(Part.abs)
Text("Legs").tag(Part.legs)
Text("Push").tag(Part.push)
Text("Pull").tag(Part.pull)
Text("Full Body").tag(Part.fullBody)
}.pickerStyle(.segmented)
ExerciseListView(predicate:config.predicate)
}
}
}
}
struct ExerciseListView: View {
// var part: String
let predicate: NSPredicate?
// #FetchRequest var exercises: FetchedResults<Exercise>
init(predicate: NSPredicate?) {
self.predicate = predicate
// self._exercises = FetchRequest(
// entity: Exercise.entity(),
// sortDescriptors: [],
//
// predicate: NSPredicate(format: "musclegroup == %#", part as any CVarArg)
// )
}
var body: some View {
Text(predicate?.description ?? "")
// List(exercises) { e in
// Text(e.exercisename)
// }
}
}
Since you are using Core Data you might want to use an Int enum in the entity for less storage and faster queries.
you could try this simple approach in ListView:
init(part: String) {
self.part = part
self._exercises = FetchRequest(
entity: Exercise.entity(),
sortDescriptors: [],
predicate: (part == "Full Body")
? nil
: NSPredicate(format: "musclegroup == %#", part as any CVarArg)
)
}

How to change the sort comparator or display Int? values on a Table with SwiftUI on macOS

I have the following table that mostly works when all the column values are strings:
struct Person : Identifiable {
let id: String // assume names are unique for this example
let name: String
let rank: String
}
#State var people = []
#State var sort = [KeyPathComparitor(\Person.rank)]
#State var selection: Person.ID? = nil
private var rankingsCancellable: AnyCancellable? = nil
init (event: Event) {
rankingsCancellable = event.rankingsSubject
.receive(on: DispatchQueue.main)
.sink { rankings in
var newRankings: [Person] = []
for ranking in rankings {
newRankings.append(Person(id: ranking.name, name: ranking.name, ranking: ranking.rank
}
newRankings.sort(by: sort)
people = newRankings
}
}
var body : some View {
Table(people, selection: $selection, sortOrder: $sort) {
TableColumn("Name", value: \Person.name)
TableColumn("Rank", value: \Person.rank)
}
.onChange(of: sortOrder) {
people.sort(using: $0)
}
}
This works well enough with one exception when the rank is unknown coming into the sink, its sent as an empty string and the empty strings sort before those with valid ranking. Ideally, I'd like to have those without a ranking sort after until their rank is received.
I've tried a couple of things, changing Person.rank to an Int but the compiler gives the typical 'too complex' error when:
TableColumn("Rank", value: \Person.rank) { Text(\($0.rank)) } // rank is an int here
Alternatively I've tried to create a custom sort comparator when creating the KeyPathComparator(\Person.rank) but the documentation around that is limited and haven't been able to suss out a workable solution.
Any ideas how to get the Table to simply display Ints (which I imagine would have to be optionals in this instance) or add a string comparator that moves empty strings below populated strings?
TIA
This is the best answer I could find, tho admit it does seem a bit like a kludge. Basically adding Int property for sorting and setting the sort to initially sort on this Person property
struct Person : Identifiable {
let id: String // assume names are unique for this example
let name: String
let rank: String
//Add rankForSort as an int which is a non displayed column
let rankForSort: Int
}
#State var people = []
//Change to initial sort to non-displayed rank field
#State var sort = [KeyPathComparitor(\Person.rankForSort)]
#State var selection: Person.ID? = nil
private var rankingsCancellable: AnyCancellable? = nil
init (event: Event) {
rankingsCancellable = event.rankingsSubject
.receive(on: DispatchQueue.main)
.sink { rankings in
var newRankings: [Person] = []
for ranking in rankings {
// Ranking now coming through in the subject as Int?
let rankString = ranking.rank != nil ? "\(ranking.rank!)" : ""
newRankings.append(Person(id: ranking.name, name: ranking.name, ranking: rankString, rankForSort: ranking.rank ?? Int.max)
}
newRankings.sort(by: sort)
people = newRankings
}
}
var body : some View {
Table(people, selection: $selection, sortOrder: $sort) {
TableColumn("Name", value: \Person.name)
TableColumn("Rank", value: \Person.rank)
}
.onChange(of: sortOrder) {
people.sort(using: $0)
}
}
Here is a way to do an initial sort and uses didSet which has the added benefit it doesn't use onChange which results in body called twice when resorting.
struct ProductsData {
var sortedProducts: [Product]
var sortOrder = [KeyPathComparator(\Product.name)] {
didSet {
sortedProducts.sort(using: sortOrder)
}
}
init() {
sortedProducts = myProducts.sorted(using: sortOrder)
}
}
struct ContentView: View {
#State private var data = ProductsData()
var body: some View {
ProductsTable(data: $data)
}
}
struct ProductsTable: View {
#Environment(\.horizontalSizeClass) private var horizontalSizeClass
#Binding var data: ProductsData
var body: some View {
Table(data.sortedProducts, sortOrder: $data.sortOrder) {
See this answer to a different question for the full sample.

Display subview from an array in SwiftUI

I am trying to present a sequence of Views, each gathering some information from the user. When users enter all necessary data, they can move to next View. So far I have arrived at this (simplified) code, but I am unable to display the subview itself (see first line in MasterView VStack{}).
import SwiftUI
protocol DataEntry {
var entryComplete : Bool { get }
}
struct FirstSubView : View, DataEntry {
#State var entryComplete: Bool = false
var body: some View {
VStack{
Text("Gender")
Button("Male") {
entryComplete = true
}
Button("Female") {
entryComplete = true
}
}
}
}
struct SecondSubView : View, DataEntry {
var entryComplete: Bool {
return self.name != ""
}
#State private var name : String = ""
var body: some View {
Text("Age")
TextField("Your name", text: $name)
}
}
struct MasterView: View {
#State private var currentViewIndex = 0
let subview : [DataEntry] = [FirstSubView(), SecondSubView()]
var body: some View {
VStack{
//subview[currentViewIndex]
Text("Subview placeholder")
Spacer()
HStack {
Button("Prev"){
if currentViewIndex > 0 {
currentViewIndex -= 1
}
}.disabled(currentViewIndex == 0)
Spacer()
Button("Next"){
if (currentViewIndex < subview.count-1){
currentViewIndex += 1
}
}.disabled(!subview[currentViewIndex].entryComplete)
}
}
}
}
I do not want to use NavigationView for styling reasons. Can you please point me in the right direction how to solve this problem? Maybe a different approach?
One way to do this is with a Base View and a switch statement combined with an enum. This is a similar pattern I've used in the past to separate flows.
enum SubViewState {
case ViewOne, ViewTwo
}
The enum serves as a way to easily remember and track which views you have available.
struct BaseView: View {
#EnvironmentObject var subViewState: SubViewState = .ViewOne
var body: some View {
switch subViewState {
case ViewOne:
ViewOne()
case ViewTwo:
ViewTwo()
}
}
}
The base view is a Container for the view control. You will likely add a view model, which is recommended, and set the state value for your #EnvironmentObject or you'll get a null pointer exception. In this example I set it, but I'm not 100% sure if that syntax is correct as I don't have my IDE available.
struct SomeOtherView: View {
#EnvironmentObject var subViewState: SubViewState
var body: some View {
BaseView()
Button("Switch View") {
subViewState = .ViewTwo
}
}
}
This is just an example of using it. You can access your #EnvironmentObject from anywhere, even other views, as it's always available until disposed of. You can simply set a new value to it and it will update the BaseView() that is being shown here. You can use the same principle in your code, using logic, to determine the view to be shown and simply set its value and it will update.

Published works for single object but not for array of objects

I am trying to make individually moveable objects. I am able to successfully do it for one object but once I place it into an array, the objects are not able to move anymore.
Model:
class SocialStore: ObservableObject {
#Published var socials : [Social]
init(socials: [Social]){
self.socials = socials
}
}
class Social : ObservableObject{
var id: Int
var imageName: String
var companyName: String
#Published var pos: CGPoint
init(id: Int, imageName: String, companyName: String, pos: CGPoint) {
self.id = id
self.imageName = imageName
self.companyName = companyName
self.pos = pos
}
var dragGesture : some Gesture {
DragGesture()
.onChanged { value in
self.pos = value.location
print(self.pos)
}
}
}
Multiple image (images not following drag):
struct ContentView : View {
#ObservedObject var socialObject: SocialStore = SocialStore(socials: testData)
#ObservedObject var images: Social = testData[2]
var body: some View {
VStack {
ForEach(socialObject.socials, id: \.id) { social in
Image(social.imageName)
.position(social.pos)
.gesture(social.dragGesture)
}
}
}
}
Single image (image follow gesture):
struct ContentView : View {
#ObservedObject var socialObject: SocialStore = SocialStore(socials: testData)
#ObservedObject var images: Social = testData[2]
var body: some View {
VStack {
Image(images.imageName)
.position(images.pos)
.gesture(images.dragGesture)
}
}
}
I expect the individual items to be able to move freely . I see that the coordinates are updating but the position of each image is not.
First, a disclaimer: The code below is not meant as a copy-and-paste solution. Its only goal is to help you understand the challenge. There may be more efficient ways of resolving it, so take your time to think of your implementation once you understand the problem.
Why the view does not update?: The #Publisher in SocialStore will only emit an update when the array changes. Since nothing is being added or removed from the array, nothing will happen. Additionally, because the array elements are objects (and not values), when they do change their position, the array remains unaltered, because the reference to the objects remains the same. Remember: Classes create objects, Structs create values.
We need a way of making the store, to emit a change when something in its element changes. In the example below, your store will subscribe to each of its elements bindings. Now, all published updates from your items, will be relayed to your store publisher, and you will obtain the desired result.
import SwiftUI
import Combine
class SocialStore: ObservableObject {
#Published var socials : [Social]
var cancellables = [AnyCancellable]()
init(socials: [Social]){
self.socials = socials
self.socials.forEach({
let c = $0.objectWillChange.sink(receiveValue: { self.objectWillChange.send() })
// Important: You have to keep the returned value allocated,
// otherwise the sink subscription gets cancelled
self.cancellables.append(c)
})
}
}
class Social : ObservableObject{
var id: Int
var imageName: String
var companyName: String
#Published var pos: CGPoint
init(id: Int, imageName: String, companyName: String, pos: CGPoint) {
self.id = id
self.imageName = imageName
self.companyName = companyName
self.pos = pos
}
var dragGesture : some Gesture {
DragGesture()
.onChanged { value in
self.pos = value.location
print(self.pos)
}
}
}
struct ContentView : View {
#ObservedObject var socialObject: SocialStore = SocialStore(socials: testData)
var body: some View {
VStack {
ForEach(socialObject.socials, id: \.id) { social in
Image(social.imageName)
.position(social.pos)
.gesture(social.dragGesture)
}
}
}
}
For those who might find it helpful. This is a more generic approach to #kontiki 's answer.
This way you will not have to be repeating yourself for different model class types.
import Foundation
import Combine
import SwiftUI
class ObservableArray<T>: ObservableObject {
#Published var array:[T] = []
var cancellables = [AnyCancellable]()
init(array: [T]) {
self.array = array
}
func observeChildrenChanges<K>(_ type:K.Type) throws ->ObservableArray<T> where K : ObservableObject{
let array2 = array as! [K]
array2.forEach({
let c = $0.objectWillChange.sink(receiveValue: { _ in self.objectWillChange.send() })
// Important: You have to keep the returned value allocated,
// otherwise the sink subscription gets cancelled
self.cancellables.append(c)
})
return self
}
}
class Social : ObservableObject{
var id: Int
var imageName: String
var companyName: String
#Published var pos: CGPoint
init(id: Int, imageName: String, companyName: String, pos: CGPoint) {
self.id = id
self.imageName = imageName
self.companyName = companyName
self.pos = pos
}
var dragGesture : some Gesture {
DragGesture()
.onChanged { value in
self.pos = value.location
print(self.pos)
}
}
}
struct ContentView : View {
//For observing changes to the array only.
//No need for model class(in this case Social) to conform to ObservabeObject protocol
#ObservedObject var socialObject: ObservableArray<Social> = ObservableArray(array: testData)
//For observing changes to the array and changes inside its children
//Note: The model class(in this case Social) must conform to ObservableObject protocol
#ObservedObject var socialObject: ObservableArray<Social> = try! ObservableArray(array: testData).observeChildrenChanges(Social.self)
var body: some View {
VStack {
ForEach(socialObject.array, id: \.id) { social in
Image(social.imageName)
.position(social.pos)
.gesture(social.dragGesture)
}
}
}
}
There are two ObservableObject types and the one that you are interested in is Combine.ObservableObject. It requires an objectWillChange variable of type ObservableObjectPublisher and it is this that SwiftUI uses to trigger a new rendering. I am not sure what Foundation.ObservableObject is used for but it is confusing.
#Published creates a PassthroughSubject publisher that can be connected to a sink somewhere else but which isn't useful to SwiftUI, except for .onReceive() of course.
You need to implement
let objectWillChange = ObservableObjectPublisher()
in your ObservableObject class

Resources