Swift Update Struct in View - swift

I apologize I am new to Swift and may be going about this completely wrong.
I am attempting to call the mutating function on my struct in my view to add additional phones or emails. This is my Struct.
struct CreateCustomer: Codable {
var Phone: [CustomerPhone]
var Emails: [String]
init() {
Phone = [CustomerPhone()]
Emails = []
}
public mutating func addPhone(){
Phone.append(CustomerPhone())
}
public mutating func addEmail(){
Emails.append("")
}
}
struct CustomerPhone: Codable {
var Phone: String
var PhoneType: Int
init(){
Phone = ""
PhoneType = 0
}
}
I am attempting to add a phone to my state var with the following
Button("Add Phone"){
$Customer_Create.addPhone()
}
I get the following Error
Cannot call value of non-function type 'Binding<() -> ()>' Dynamic key
path member lookup cannot refer to instance method 'addPhone()'
Thank you for any help!

If Customer_Create is a state property variable (like below) then you don't need binding, use property directly
struct ContentView: View {
#State private var Customer_Create = CreateCustomer()
var body: some View {
Button("Add Phone"){
Customer_Create.addPhone() // << here !!
}
}
}

You shouldn't be accessing the Binding via $, you should simply access the property itself.
Button("Add Phone"){
Customer_Create.addPhone()
}
Unrelated to your question, but you should be conforming to the Swift naming convention, which is lowerCamelCase for variables and properties - so customerCreate, not Customer_Create.

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)

Using SwiftUI ForEach to iterate over [any Protocol] where said protocol is Identifiable

In a ViewModel I have:
public var models: [any Tabbable]
And Tabbable starts with
public protocol Tabbable: Identifiable {
associatedtype Id
var id: Id { get }
/// ...
}
In Swift in my ViewModel I can use the Array.forEach to:
models.forEach { _ in
print("Test")
}
But in SwiftUI I can't use ForEach to:
ForEach(viewModel.models) { _ in
Text("Test")
}
Due to:
Type 'any Tabbable' cannot conform to 'Identifiable'
Any suggestions?
This is with Swift 5.7. Do I need to go back to making something like AnyTabbable?
Thanks in advance.
Consider this example in the form of a Playground:
import UIKit
import SwiftUI
public protocol Tabbable: Identifiable {
associatedtype Id
var id: Id { get }
var name : String { get }
}
struct TabbableA : Tabbable{
typealias Id = Int
var id : Id = 3
var name = "TabbableA"
}
struct TabbableB : Tabbable {
typealias Id = UUID
var id : Id = UUID()
var name = "TabbableB"
}
struct ViewModel {
public var models: [any Tabbable]
}
let tabbableA = TabbableA()
let tabbableB = TabbableB()
let models: [any Tabbable] = [tabbableA, tabbableB]
struct ContentView {
#State var viewModel : ViewModel = ViewModel(models: models)
var body : some View {
ForEach(viewModel.models) { model in
Text(model.name)
}
}
}
In this case, we have the type TabbableA where each instance has an id property that is an integer (for the sake of the sample they only use "3" but it's the type that is significant). In TabbableB each instance's id is a UUID. Then I create an array where one item is an instance of TabableA and another is an instance of TabbableB. The array is of type [any Tabbable].
Then I try to use ForEach on the array. But some elements in the array use Int ids and some use UUID ids. The system doesn't know what type to use to uniquely identify views. Ints and UUIDs can't be directly compared to one another to determine equality. While each item that went into the array is Tabbable, and therefore conforms to Identifiable, the elements coming out of the array, each of which is of type any Tabbable, do not conform to Identifiable. So the system rejects my code.
You can provide a KeyPath to the id (or any unique variable): parameter which specifies how to retrieve the ID in ForEach. It is because all items in ForEach must be unique. You can create a structure, which conforms to your protocol. At that moment it should work. The second option is to remove Identifiable from the protocol and then you can use that directly.
public protocol Tabbable {
var id: String { get }
/// ...
}
public var models: [Tabbable]
ForEach(viewModel.models, id: \.id) { _ in
Text("Test")
}
or
public protocol TabbableType: Identifiable {
associatedtype Id
var id: Id { get }
/// ...
}
struct Tabbable: TabbableType {
var id: String { get }
/// ...
}
public var models: [Tabbable]
ForEach(viewModel.models) { _ in
Text("Test")
}

SwiftUI - #AppStorage variable key value

I'm trying to get from UserDefaults a Bool whose key depends on the nameID of a Product.
I tried this way:
import SwiftUI
struct ProductDetail: View {
var product: GumroadProduct
#AppStorage("LOCAL_LIBRARY_PRESENCE_PRODUCTID_\(product.id)") var isLocal: Bool = false
var body: some View {
Text("ProductView")
}
}
Anyway Swift throws this error:
Cannot use instance member 'product' within property initializer;
property initializers run before 'self' is available
I understand why Swift is throwing that error but I don't know how to get around it.
Is there a solution?
Here is a solution for your code snapshot - provide explicit initialiser and instantiate properties in it depending on input:
struct ProductDetail: View {
#AppStorage private var isLocal: Bool
private var product: GumroadProduct
init(product: GumroadProduct) {
self.product = product
self._isLocal = AppStorage(wrappedValue: false, "LOCAL_LIBRARY_PRESENCE_PRODUCTID_\(product.id)")
}
var body: some View {
Text("ProductView")
}
}

PropertyWrappers and protocol declaration?

Using Xcode 11 beta 6, I am trying to declare a protocol for a type with properties using #Published (but this question can be generalized to any PropertyWrapper I guess).
final class DefaultWelcomeViewModel: WelcomeViewModel & ObservableObject {
#Published var hasAgreedToTermsAndConditions = false
}
For which I try to declare:
protocol WelcomeViewModel {
#Published var hasAgreedToTermsAndConditions: Bool { get }
}
Which results in a compilation error: Property 'hasAgreedToTermsAndConditions' declared inside a protocol cannot have a wrapper
So I try to change it into:
protocol WelcomeViewModel {
var hasAgreedToTermsAndConditions: Published<Bool> { get }
}
And trying
Which does not compile, DefaultWelcomeViewModel does not conform to protocol, okay, so hmm, I cannot using Published<Bool> then, let's try it!
struct WelcomeScreen<ViewModel> where ViewModel: WelcomeViewModel & ObservableObject {
#EnvironmentObject private var viewModel: ViewModel
var body: some View {
// Compilation error: `Cannot convert value of type 'Published<Bool>' to expected argument type 'Binding<Bool>'`
Toggle(isOn: viewModel.hasAgreedToTermsAndConditions) {
Text("I agree to the terms and conditions")
}
}
}
// MARK: - ViewModel
protocol WelcomeViewModel {
var hasAgreedToTermsAndConditions: Published<Bool> { get }
}
final class DefaultWelcomeViewModel: WelcomeViewModel & ObservableObject {
var hasAgreedToTermsAndConditions = Published<Bool>(initialValue: false)
}
Which results in the compilation error on the Toggle: Cannot convert value of type 'Published<Bool>' to expected argument type 'Binding<Bool>'.
Question: How can I make a protocol property for properties in concrete types using PropertyWrappers?
I think the explicit question you're asking is different from the problem you are trying to solve, but I'll try to help with both.
First, you've already realized you cannot declare a property wrapper inside a protocol. This is because property wrapper declarations get synthesized into three separate properties at compile-time, and this would not be appropriate for an abstract type.
So to answer your question, you cannot explicitly declare a property wrapper inside of a protocol, but you can create individual property requirements for each of the synthesized properties of a property wrapper, for example:
protocol WelcomeViewModel {
var hasAgreed: Bool { get }
var hasAgreedPublished: Published<Bool> { get }
var hasAgreedPublisher: Published<Bool>.Publisher { get }
}
final class DefaultWelcomeViewModel: ObservableObject, WelcomeViewModel {
#Published var hasAgreed: Bool = false
var hasAgreedPublished: Published<Bool> { _hasAgreed }
var hasAgreedPublisher: Published<Bool>.Publisher { $hasAgreed }
}
As you can see, two properties (_hasAgreed and $hasAgreed) have been synthesized by the property wrapper on the concrete type, and we can simply return these from computed properties required by our protocol.
Now I believe we have a different problem entirely with our Toggle which the compiler is happily alerting us to:
Cannot convert value of type 'Published' to expected argument type 'Binding'
This error is straightforward as well. Toggle expects a Binding<Bool>, but we are trying to provide a Published<Bool> which is not the same type. Fortunately, we have chosen to use an #EnvironmentObject, and this enables us to use the "projected value" on our viewModel to obtain a Binding to a property of the view model. These values are accessed using the $ prefix on an eligible property wrapper. Indeed, we have already done this above with the hasAgreedPublisher property.
So let's update our Toggle to use a Binding:
struct WelcomeView: View {
#EnvironmentObject var viewModel: DefaultWelcomeViewModel
var body: some View {
Toggle(isOn: $viewModel.hasAgreed) {
Text("I agree to the terms and conditions")
}
}
}
By prefixing viewModel with $, we get access to an object that supports "dynamic member lookup" on our view model in order to obtain a Binding to a member of the view model.
I would consider another solution - have the protocol define a class property with all your state information:
protocol WelcomeViewModel {
var state: WelcomeState { get }
}
And the WelcomeState has the #Published property:
class WelcomeState: ObservableObject {
#Published var hasAgreedToTermsAndConditions = false
}
Now you can still publish changes from within the viewmodel implementation, but you can directly observe it from the view:
struct WelcomeView: View {
#ObservedObject var welcomeState: WelcomeState
//....
}