Change a #Published var via Picker - swift

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.

Related

Trying to dynamically update swiftUI view

struct MakeVideo: View {
#EnvironmentObject var modelData: ModelData
#State private var chosenFriends: [FriendModel] = []
mutating func addFriend(_friend: FriendModel) -> Void {
chosenFriends.append(_friend)
}
var body: some View {
VStack {
ForEach(modelData.friends) { friend in
HStack {
ProfilePic(picture: friend.profilepic!)
Text("#"+friend.username!)
//TODO: This is updating the val, it just isn't being shown here
Button("Add", action: friend.toggleChosen)
if friend.isChosen {
Image(systemName: "star.fill")
.foregroundColor(.yellow)
} else {
Image(systemName: "star")
}
}
}
}
}
}
struct MakeVideo_Previews: PreviewProvider {
static var previews: some View {
MakeVideo()
.environmentObject(ModelData())
}
}
I am trying to dynamically update this so that when ai click the Button, it'll make the star be filled instead of empty. In the debugger I see the class value being changed however the change does not appear in the view. I also made the var in the class #Published, which I thought would allow the view to change with the value
Here is my code for the classes and ModelData
class FriendModel: Identifiable, ObservableObject {
init(id: Int, username: String, profilepic: String) {
self.id = id
self.username = username
self.profilepic = profilepic
}
var id: Int?
var username: String?
var profilepic: String?
#Published var isChosen = false
//var profilepic: UIImage
func toggleChosen() {
print(self.isChosen)
self.isChosen = !self.isChosen
print(self.isChosen)
}
}
var allvideos: [VideoModel] = [VideoModel(id: 1, name: "Beach Trip", length: 25, url: "mona"), VideoModel(id: 2, name: "Dogs", length: 10, url:"dog"), VideoModel(id: 3, name: "Concerts", length: 42, url: "hogrider")]
var allfriends: [FriendModel] = [FriendModel(id: 1, username: "bpaul18", profilepic: "profilepic"), FriendModel(id: 2, username: "kmill", profilepic: "profilepic"), FriendModel(id: 3, username: "dudeitsdom", profilepic: "profilepic")]
final class ModelData: ObservableObject {
#Published var videos: [VideoModel] = allvideos
#Published var friends: [FriendModel] = allfriends
}
You don't say that you get a compiler error on the following line:
Button("Add", action: friend.toggleChosen)
Therefore I deduce that FriendModel is a class, not a struct. If FriendModel were a struct and toggleChosen were a mutating method, then you would get an error: “Cannot use mutating member on immutable value”.
Even if FriendModel conforms to ObservableObject, the problem is that ObservableObjects do not automatically compose. A change to an #Published property of a FriendModel will not be noticed by a containing ModelData, which means (in this case) that SwiftUI will not notice that friend.isChosen was modified.
I suggest making FriendModel into a struct.
I also recommend using Point-Free's Composable Architecture package (or something similar) as your app architecture, because it provides a comprehensive solution to problems like this.
try using this:
ForEach($modelData.friends) { $friend in // <-- here $
Body is only called for states that are used. Since chosenFriends is not used in body, it is not called when it is changed in a button action.
To fix, write a func isFriend and lookup the friend ID in the chosenFriends array and use the result of that in body to show the star. Since chosenFriends is now used, body will be called. Also change the button to call the addFriend func which would be better named as chooseFriend by the way.

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)

Referencing a nested enum/type outside of the parent (struct/type) where the parent is generic

I have a View Component (MyViewWrapper) that has a nested enum (Value):
struct MyViewWrapper<Content: View>: View {
#Binding var value: Value
let content: Content
public init(value: Binding<Value>, #ViewBuilder content: #escaping () -> Content) {
self._value = value
self.content = content()
}
var body: some View {
content
}
enum Value {
case percent(Double)
case points(Double)
}
}
Referencing the enum (Value) inside the component isn't an issue, but when I try to instantiate a Value outside of the struct:
struct SampleView: View {
#State var value: MyViewWrapper.Value = .percent(0.5)
var body: some View {
MyViewWrapper(value: $value) {
Color.red
}
}
}
I get a Generic parameter 'Content' could not be inferred error, as the parent struct/type is generic.
Sure, I could just extract the enum and call it something like MyViewWrapperValue. But I would really like to reference like so:
#State var value: MyViewWrapper.Value = .percent(0.5)
Is this possible?
Nested types within generic types are a pitfall in the language. You always have to define a placeholder for the outer type.
If your code is simple enough that you actually know what Content is, you can use the horrible (but compiling) option of using it explicitly:
#State var value: MyViewWrapper<Color>.Value = .percent(0.5)
A more scalable approach is to keep it as a nested type that refers to something concrete. E.g.
struct MyViewWrapper<Content: View>: View {
typealias Value = MyViewWrapper<Never>._Value
extension MyViewWrapper where Content == Never {
enum _Value {
case percent(Double)
case points(Double)
}
}
Then, you can use Value or _Value interchangeably elsewhere.
struct SampleView: View {
#State var value: MyViewWrapper<Never>.Value = .percent(0.5)
Whether that's better than a non-nested namespace-cluttering MyViewWrapperValue is debatable.

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

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

SwiftUI navigate if ObservedObject property changes

I can't for the life of me figure out how to only navigate if a property from my ObservedObject changes to meet a condition. ie when my state changes to some condition, navigate to the next screen.
I've used the tag and selection initializer on the NavigationLink but it selection requires a Binding, and I can't derive a Binding from the properties on my ObservedObject without using the .constant() initializer on Binding which is only an immutable value.
#ObservedObject var store: Store<AppState, AppValue>
NavigationLink(
destination: SecondView(),
tag: true,
selection: store.shouldNavigate // Can't do this because I need a binding
)
How else are people implementing buttons that only navigate if a condition in their state is met? I'm trying to avoid using the #State because I want the navigation to depend on my app state not on a local state that I'm toggling based on some business logic
public final class Store<Value, Action>: ObservableObject {
#Published public private(set) var value: Value
}
UPDATE:
So it looks like I should be able to create a binding but since store.value gives me Binding<Value> I get an error: Generic parameter Subject cannot be inferred.
just remove private(set) in your model, NavigationLink will set shouldNavigate to false after navigation is completed, so it should not be private(set)
public final class Store<Value, Action>: ObservableObject {
#Published public var value: Value
}
I show you an important intermediate solution before generics. I think the key here is that selection binding requires optional binding. That's most hassles coming from.
enum AppState: String{
case none = "none"
case red = "red"
case blue = "blue"
case green = "green"
case purple = "purple"
}
enum AppValue: String{
case none
}
public final class Store<V, A>: ObservableObject {
#Published var value: AppState? = AppState.none
public var link :Color = Color.white
init(value: AppState = .none, link : Color = Color.white){
self.link = link
self.value = value
}
}
struct TestView: View {
#ObservedObject var store: Store<AppState?, AppValue>
var viewStates: [Store<AppState?,AppValue>] =
[Store(value: .red, link: Color.red),
Store(value: .blue, link: Color.blue) ,
Store(value: .green, link: Color.green),
Store(value: .purple, link: Color.purple) ]
var body: some View {
NavigationView{
VStack{
ForEach(viewStates, id: \.value){ s in
Group{
NavigationLink(destination: s.link, tag: s.value!, selection: self.$store.value ){EmptyView()}
Button( s.value!.rawValue, action: {
self.store.value = s.value!})
}}
}
}
}
}