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.
Related
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)
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.
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")
}
}
}
}
}
I'm developing a simple SwiftUI app in Xcode 11. I want to have a form that loops through multiple user input strings and displays a form with a button. When the user presses the button it modifies the input value - specifically increment or decrement it.
However when passing an array of references like UserInput().foo where UserInput is a published observable object I cannot modify the value inside a ForEach because the ForEach is passed a copy as oppose to the original reference (at least that's my basic understanding). How do I then try to achieve it? I read about inout and everybody says to avoid it but surely this must be a relatively common issue.
I've made an simple example of what I'm trying to do but I can't quite work it out:
import SwiftUI
class UserInput: ObservableObject {
#Published var foo: String = ""
#Published var bar: String = ""
}
struct ContentView: View {
#ObservedObject var input = UserInput()
var body: some View {
LoopInputs()
}
func LoopInputs() -> AnyView?{
var userinputs = [
[UserInput().foo, "Foo"],
[UserInput().bar, "Bar"]
]
var inputs: some View{
VStack(){
ForEach(userinputs, id: \.self){userinput in
Text("\(userinput[1]): \(String(userinput[0]))")
Button(action: {
increment(input: String(userinput[0]))
}){
Text("Increase")
}
}
}
}
return AnyView(inputs)
}
func increment(input: String){
var lead = Int(input) ?? 0
lead += 1
// input = String(lead)
}
}
As I understood, when adding a value to userinputs, the ForEach values doesn't change.
Well, if that's the case, first of all, you could try creating a struct and in it, you declare foo and bar, then just declare a variable of type the struct. It'll look like this:
struct Input: Identifiable {
var id = UUID()
var foo: String
var bar: String
}
class UserInput: ObservableObject {
#Published var inputs: [Input] = [Input]()
}
//ContentView
struct ContentView: View {
#ObservedObject var input = UserInput()
var body: some View {
LoopInputs()
}
func LoopInputs() -> AnyView? {
var inputs: some View {
VStack {
ForEach(input.inputs) { userinput in
Text("\(userinput.bar): \(String(userinput.foo))")
Button(action: {
increment(input: String(userinput.foo))
}) {
Text("Increase")
}
}
}
}
return AnyView(inputs)
}
func increment(input: String) {
var lead = Int(input) ?? 0
lead += 1
// input = String(lead)
}
}
Wouldn't this be easier and more elegant?
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!})
}}
}
}
}
}