How to bind a #publish datamodel in SwiftUI? - swift

Is there any way to bind a data model in swiftui?
I have coded like below and need to build a struct so that I can use it in multiple views but the problem is to know how to bind a #publish data model in swiftui?
var birds: [PlayerItem] = [PlayerItem(id: UUID(), playershow: false)]
var dogs: [PlayerItem] = [PlayerItem(id: UUID(), playershow: true)]
class Controller: ObservableObject {
#Published var bird = birds
#Published var dog = dogs
}
struct PlayerItem: Hashable {
var id = UUID()
var playerShow: Bool
}
struct ContentView: View {
#EnvironmentObject var control: Controller
var body: some View {
setButton(isOn: $Controller.bird)
}
}
struct setButton: View {
#Binding var isOn: [PlayerItem]
var body: some View {
Button(action: {
self.isOn[0].toggle()
}) {
Text(isOn[0] ? "Off" : "On")
}
}
}
I wrote the following code:
#Binding var isOn: [PlayerItem]
However, it complained the following:
Value of type 'EnvironmentObject<controller>.Wrapper' has no dynamic member 'isOn' using the key path from the root type 'Controller'

try the following code, it shows how to use #Binding and how you have to use playershow
class Controller: ObservableObject {
#Published var bird = [Playeritem(id: UUID(), playershow: false)]
#Published var dog = [Playeritem(id: UUID(), playershow: true)]
}
struct Playeritem: Hashable {
var id = UUID()
var playershow: Bool
}
struct ContentView: View {
#StateObject var control = Controller() // <-- for testing
var body: some View {
setButton(isOn: $control.bird) // <-- here control
}
}
struct setButton: View {
#Binding var isOn: [Playeritem]
var body: some View {
Button(action: {
self.isOn[0].playershow.toggle() // <-- here playershow
}) {
Text(isOn[0].playershow ? "Off" : "On") // <-- here playershow
}
}
}

Related

How to observer a property in swift ui

How to observe property value in SwiftUI.
I know some basic publisher and observer patterns. But here is a scenario i am not able to implement.
class ScanedDevice: NSObject, Identifiable {
//some variables
var currentStatusText: String = "Pending"
}
here CurrentStatusText is changed by some other callback method that update the status.
Here there is Model class i am using
class SampleModel: ObservableObject{
#Published var devicesToUpdated : [ScanedDevice] = []
}
swiftui component:
struct ReviewView: View {
#ObservedObject var model: SampleModel
var body: some View {
ForEach(model.devicesToUpdated){ device in
Text(device.currentStatusText)
}
}
}
Here in UI I want to see the real-time status
I tried using publisher inside ScanDevice class but sure can to use it in 2 layer
You can observe your class ScanedDevice, however you need to manually use a objectWillChange.send(),
to action the observable change, as shown in this example code.
class ScanedDevice: NSObject, Identifiable {
var name: String = "some name"
var currentStatusText: String = "Pending"
init(name: String) {
self.name = name
}
}
class SampleViewModel: ObservableObject{
#Published var devicesToUpdated: [ScanedDevice] = []
}
struct ReviewView: View {
#ObservedObject var viewmodel: SampleViewModel
var body: some View {
VStack (spacing: 33) {
ForEach(viewmodel.devicesToUpdated){ device in
HStack {
Text(device.name)
Text(device.currentStatusText).foregroundColor(.red)
}
Button("Change \(device.name)") {
viewmodel.objectWillChange.send() // <--- here
device.currentStatusText = UUID().uuidString
}.buttonStyle(.bordered)
}
}
}
}
struct ContentView: View {
#StateObject var viewmodel = SampleViewModel()
var body: some View {
ReviewView(viewmodel: viewmodel)
.onAppear {
viewmodel.devicesToUpdated = [ScanedDevice(name: "device-1"), ScanedDevice(name: "device-2")]
}
}
}

SwiftUI Loading Data

I am trying to load data from an ObservableObject class when I present a SwiftUI view but I get the following error code: "Cannot use instance member 'documentID' within property initializer; property initializers run before 'self' is available"
Class with the data querying:
class ItemDataDelegate: ObservableObject {
#Published var serviceContentLoaded = Bool()
#Published var SelectedCategory: String?
#Published var SelectedDocumentID: String?
init(SelectedCategory: String, SelectedDocumentID: String){
self.getSelectedContentData(Category: SelectedCategory, DocumentID: SelectedDocumentID) {
self.getSelectedCompanyData(CUID: ContentData[indexPath.row].CompanyID) {
print("data query completed")
}
}
}
}
Here is the View that I want to present when the data from the previous code is called and completed:
The error I mentioned above is shown on the #StateObject var itemDataDelegate = ItemDataDelegate(SelectedCategory: selectedCategory, SelectedDocumentID: documentID) line of code.
struct DetailView: View {
#Binding var documentID: String
#Binding var selectedCategory: String
#StateObject var itemDataDelegate = ItemDataDelegate(SelectedCategory: selectedCategory, SelectedDocumentID: documentID)
#State private var showSheet = false
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
var image: UIImage
var companyName: String
var cartActive: Bool
var body: some View {
return ZStack {
switch itemDataDelegate.serviceContentLoaded {
case true:
ContentView()
case false:
LoadingView()
}
}
.navigationBarHidden(true)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.edgesIgnoringSafeArea(.all)
}
}
Since you need to use one of the passed-in parameters in the initialization of the #StateObject, you'll have to write a custom init for your View. Using your current code, it would look something like this:
struct DetailView: View {
#Binding var documentID: String
#Binding var selectedCategory: String
#StateObject var itemDataDelegate : ItemDataDelegate
#State private var showSheet = false
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
var image: UIImage
var companyName: String
var cartActive: Bool
init(documentID: Binding<String>, selectedCategory: Binding<String>, image: UIImage, companyName: String, cartActive: Bool) {
_documentID = documentID
_selectedCategory = selectedCategory
self.image = image
self.companyName = companyName
self.cartActive = cartActive
_itemDataDelegate = StateObject(wrappedValue: ItemDataDelegate(SelectedCategory: selectedCategory.wrappedValue, SelectedDocumentID: documentID.wrappedValue))
}
var body: some View {
return ZStack {
switch itemDataDelegate.serviceContentLoaded {
case true:
ContentView()
case false:
LoadingView()
}
}
.navigationBarHidden(true)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.edgesIgnoringSafeArea(.all)
}
}

SwiftUI Sharing model data between views

I have such a structure, this structure seems wrong to me as the approach I want to ask you. So when I want to use 2 models in 1 view, I have to put it in foreach in one view. This is what I want. Using the data I use in my user's profile on other pages I want. How should I do this? How do you guys do it?
Let me give an example for your better understanding:
I want to show my user's Username data on the Homepage, how should I do this?. In fact, after initializing my model once, I want to use it in other views. What is the right approach.
import SwiftUI
struct ContentView: View {
#StateObject var network = ProfileNetwork()
var body: some View {
TabView{
ProfileView().tabItem { Image(systemName: "house") }
ForEach(self.network.userprofile,id:\.id){a in
ShopView(profile_model: a)
}.tabItem { Image(systemName: "house") }
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class ProfileNetwork : ObservableObject {
#Published var userprofile : [UserPRofile] = [UserPRofile(name: "Test", coin: 1, id: "dsa")]
}
struct ProfileView : View {
#StateObject var network = ProfileNetwork()
var body: some View {
ForEach(self.network.userprofile, id:\.id){ i in
ProfileViewModel(profile_model: i)
}
}
}
struct ProfileViewModel : View {
var profile_model : UserPRofile
var body: some View {
Text(self.profile_model.name)
}
}
struct UserPRofile : Decodable{
var name : String
var coin : Int
var id : String
}
class ShopeNetwork : ObservableObject {
#Published var shop : [ShopStore] = [ShopStore(id: "sda", image: "dasd", price: 100, name: "sda")]
}
struct ShopView : View {
#StateObject var network = ShopeNetwork()
var profile_model : UserPRofile
var body: some View {
ForEach(self.network.shop, id:\.id){ c in
ShopViewModel(shop_model: c, profile_model: profile_model)
}
}
}
struct ShopViewModel : View {
var shop_model : ShopStore
var profile_model : UserPRofile
var body: some View {
Text(profile_model.name)
Text(self.shop_model.name)
}
}
struct ShopStore : Decodable {
var id : String
var image : String
var price : Int
var name : String
}
A possible solution is to create an #EnvironmentObject and inject it at the root level:
class AppState: ObservableObject {
#Published var userProfile: UserPRofile?
}
#main
struct TestApp: App {
#StateObject private var appState = AppState()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(appState)
}
}
}
struct ProfileView: View {
#EnvironmentObject private var appState: AppState // access as an `#EnvironmentObject`
#StateObject var network = ProfileNetwork()
var body: some View {
VStack {
ForEach(self.network.userprofile, id: \.id) { i in
ProfileViewModel(profile_model: i)
}
}
.onAppear {
appState.userProfile = network.userprofile.first // set `userProfile` globally
}
}
}
struct ShopView: View {
#EnvironmentObject private var appState: AppState // use it in any other view
...
Swift 5, iOS 14
Make the class a singleton so it becomes shareable easily.
class ProfileNetwork : ObservableObject {
#Published var userprofile : [UserPRofile] = [UserPRofile(name: "Test", coin: 1, id: "dsa")]
static var shared = ProfileNetwork()
}
And then refer to it with the shared handle.
struct ContentView: View {
#StateObject var network = ProfileNetwork.shared
var body: some View {
....
}
struct ProfileView : View {
#StateObject var network = ProfileNetwork.shared
var body: some View {
....
}

Init for a SwiftUI class with a #Binding var

I have a class which I want to initialize with a Binding var that is set in another View.
View ->
struct CoverPageView: View {
#State var numberOfNumbers:Int
var body: some View {
NavigationView {
GeometryReader { geometry in
VStack(alignment: .center, spacing: 0){
TextField("Multiplication Upto:", value: self.$numberOfNumbers, formatter: NumberFormatter())
}
}
}
}
CLASS WHICH NEEDS TO BE INITIALIZED USING THE #Binding var $numberofNumbers -
import SwiftUI
class MultiplicationPractice:ObservableObject {
#Binding var numberOfNumbers:Int
var classNumofNumbers:Int
init() {
self.classNumofNumbers = self.$numberOfNumbers
}
}
The init statement obviously gives the error that self is not initialized and the instance var is being used to initialize which is not allowed.
How do I circumvent this? The class needs to be initialized with the number the user enters on the first view. I have written approx. code here so ignore any typos please.
Typically you'd initialize MultiplicationPractice in CoverPageView with a starting value:
#ObservedObject var someVar = MultiplicationPractice(NoN:123)
And of course, add a supporting init statement:
class MultiplicationPractice:ObservableObject {
init(NoN: Int) {
self.numberOfNumbers = val
}
and you wouldn't want to wrap your var with #Binding, instead wrap it with #Published:
class MultiplicationPractice:ObservableObject {
#Published var numberOfNumbers:Int
...
In your particular case I would even drop the numberOfNumbers var in your CoverPageView, and instead use the direct variable of the above someVar:
struct CoverPageView: View {
//removed #State var numberOfNumbers:Int
#ObservedObject var someVar = MultiplicationPractice(123)
...
TextField("Multiplication Upto:", value: self.$someVar.numberOfNumbers, formatter: NumberFormatter())
You'll notice that I passed in the sub-var of the #ObservedObject as a binding. We can do this with ObservableObjects.
Edit
I see now what you're trying to do, you want to pass a binding along across your ViewModel, and establish an indirect connection between your view and model. While this may not be the way I'd personally do it, I can still provide a working example.
Here is a simple example using your struct names:
struct MultiplicationGame {
#Binding var maxNumber:String
init(maxNumber: Binding<String>) {
self._maxNumber = maxNumber
print(self.maxNumber)
}
}
class MultiplicationPractice:ObservableObject {
var numberOfNumbers: Binding<String>
#Published var MulGame:MultiplicationGame
init(numberOfNumbers: Binding<String> ) {
self.numberOfNumbers = numberOfNumbers
self.MulGame = MultiplicationGame(maxNumber: numberOfNumbers)
}
}
struct ContentView: View {
#State var someText: String
#ObservedObject var mulPractice: MultiplicationPractice
init() {
let state = State(initialValue: "")
self._someText = state
self.mulPractice = MultiplicationPractice(numberOfNumbers: state.projectedValue)
}
var body: some View {
TextField("put your text here", text: $someText)
}
}
Okay, I don't really understand your question so I'm just going to list a few examples and hopefully one of them will be what you're looking for.
struct SuperView: some View {
#State var value: Int = 0
var body: some View {
SubView(value: self.$value)
}
}
struct SubView: View {
#Binding var value: Int
// This is the same as the compiler-generated memberwise initializer
init(value: Binding<Int>) {
self._value = value
}
var body: some View {
Text("\(value)")
}
}
If I misunderstood and you're just trying to get the current value, do this
struct SuperView: some View {
#State var value: Int = 0
var body: some View {
SubView(value: self.value)
}
}
struct SubView: View {
let value: Int
// This is the same as the compiler-generated memberwise initializer
init(value: Int) {
self.value = value
}
var body: some View {
Text("\(value)")
}
}

Updating Picker on update of ObservedObject

I'm trying up dynamically add new rows to a Picker as follows:
class ViewModel: ObservableObject {
#Published private (set) var drinks = ["Tea", "Coffee", "Wine"]
func addDrink(_ drink: String) {
drinks.append(drink)
}
}
struct PickerTest: View {
#State private var selectedDrink = "Tea"
#State private var customDrink = ""
#ObservedObject private var viewModel = ViewModel()
var body: some View {
VStack {
HStack {
TextField("Enter a drink", text: $customDrink)
Spacer()
Button("Add") {
self.viewModel.addDrink(self.customDrink)
}
}
Picker("Drinks", selection: $selectedDrink) { // Removing the wrapping Picker works
ForEach(viewModel.drinks, id: \.self) { drink in
Text(drink)
}
}
}.padding().labelsHidden()
}
}
This doesn't work. If I remove the Picker wrapping the ForEach, the ForEach updates as expected.
Is there a way to update the Picker dynamically?
It looks like Pickers bug - I hope, that Apple fixes it in future releases of SwiftUI.
I found ugly (I really don't like it) workaround for this problem:
class ViewModel: ObservableObject {
#Published var selectedDrink = "Tea"
#Published var drinks = ["Tea", "Coffee", "Wine"]
#Published var drinksChanged = true
func addDrink(_ drink: String) {
drinks.append(drink)
drinksChanged.toggle()
}
}
struct DrinksPicker: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
Picker("Drinks", selection: $viewModel.selectedDrink) {
ForEach(viewModel.drinks, id: \.self) { drink in
Text(drink)
}
}
}
}
struct PickerTest: View {
#State private var customDrink = ""
#ObservedObject private var viewModel = ViewModel()
var body: some View {
VStack {
HStack {
TextField("Enter a drink", text: $customDrink)
Spacer()
Button("Add") {
self.viewModel.addDrink(self.customDrink)
self.customDrink = ""
}
}
if viewModel.drinksChanged {
DrinksPicker(viewModel: viewModel)
} else {
DrinksPicker(viewModel: viewModel)
}
}.padding().labelsHidden()
}
}
You can also hide this if-else in some another container:
struct DrinksPickerContainer: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
Group {
if viewModel.drinksChanged {
DrinksPicker(viewModel: viewModel)
} else {
DrinksPicker(viewModel: viewModel)
}
}
}
}
and then use only DrinksPickerContainer(viewModel: viewModel) in PickerTest