SwiftUI can't bind class array parameter - swift

In my SwiftUI project I have a button class model:
import SwiftUI
import Combine
class Button: Identifiable, ObservableObject {
var id = UUID()
#Published var title = String()
init(title: String) {
self.title = title
}
func changeTitle(title: String) {
self.title = title
}
}
Then I have another class named ControlPanel which has as a parameter a Button array.
class ControlPanel: Identifiable, ObservableObject {
var id = UUID()
#Published var name = String()
#Published var buttons:[Button] = []
init(name: String) {
self.name = name
}
}
I have to listen to this array in a custom collection view I have built with a UIViewControllerRepresentable class:
struct ButtonCollectionView: UIViewControllerRepresentable {
#Binding var buttons: [Button]
func makeUIViewController(context: Context) -> UICollectionViewController {
let vc = CollectionViewController(collectionViewLayout: .init())
vc.buttonArray = buttons
return vc
}
func updateUIViewController(_ uiViewController: UICollectionViewController, context: Context) {
if let vc = uiViewController as? CollectionViewController {
vc.buttonArray = buttons
vc.collectionView.reloadData()
}
}
}
Lastly, I call for this collection view in my content view, the following way:
#ObservedObject var controlPanel: ControlPanel = ControlPanel(name: "test")
var body: some View {
ButtonCollectionView(button: $controlPanel.buttons)
}
When I load my content view I do get all the cells, but once I change them, the view won't get updated. How can I fix this issue?

Your Button is already inside another #Published property and thus #Published var title won't work.
The simplest solution is to make your Button a struct (and rename it to something not used by SwiftUI, eg. CustomButton):
struct CustomButton: Identifiable {
var id = UUID()
var title = ""
mutating func changeTitle(title: String) {
self.title = title
}
}
For more advanced solutions see: How to tell SwiftUI views to bind to nested ObservableObjects

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

How to retrieve value from Class to textfield Swiftui

This is a web browser app. I want to get the url of the current web page loaded, 'urlObservation', from the init and update the textfield of the url address bar with that string. But i'm not sure how to do it.
WebView
struct WebView: NSViewRepresentable {
.........
class Coordinator: NSObject, WKUIDelegate, WKNavigationDelegate,ObservableObject {
private var viewModel: WebStateModel
private var wkWebview = WKWebView()
var urlObservation: NSKeyValueObservation?
init(_ viewModel: WebStateModel, _ wkWebview: WKWebView) {
self.viewModel = viewModel
self.wkWebview = WKWebView()
self.urlObservation = wkWebview.observe(\.url, changeHandler: { (wkWebview, change) in
print((wkWebview.url?.absoluteString ?? "Empty"))
})
}
......
}
ContentView
#State var webView: WebView?
#State var text = ""
.........
//Address Bar
TextField("Enter a URL", text: Binding(
get: { text },
set: { text = WebStateModel.stripHttps($0) } ), onCommit: {
webModel.updateUrl(text)
})
Update
I've tried adding self.text inside the changeHandler to retrieve the current url change but it returns nothing
class Coordinator: NSObject, WKUIDelegate, WKNavigationDelegate,ObservableObject {
private var viewModel: WebStateModel
private var wkWebview = WKWebView()
var urlObservation: NSKeyValueObservation?
#Published var text = ""
init(_ viewModel: WebStateModel, _ wkWebview: WKWebView) {
self.viewModel = viewModel
self.wkWebview = WKWebView()
super.init()
self.urlObservation = wkWebview.observe(\.url, changeHandler: { (wkWebview, change) in
print((wkWebview.url?.absoluteString ?? "Empty"))
self.text = ((wkWebview.url?.absoluteString ?? "Empty"))
})
}
Add the property text to the webModel shared between your View & WebView use it instead of the existing text, then update it where you need to. Just don't forget to mark it using #Published.

SwiftUI - changes in nested View Model classes not detected using onChange method

I have a nested View Model class WatchDayProgramViewModel as an ObservableObject. Within WatchDayProgramViewModel, there is a WorkoutModel that is a child class. I want to detect any updates in the currentHeartRate to trigger data transfer to iPhone.
Hence, I tried from ContentView using WatchDayProgramViewModel as an EnvironmentObject and detecting changes in WorkoutModel via onChange() method. But it seems that SwiftUI views does not detect any property changes in WorkoutModel.
I understand that this issue could be due to ObservableObject not detecting changes in child/nested level of classes, and SO answer (SwiftUI change on multilevel children Published object change) suggests using struct instead of class. But changing WorkoutModel to struct result in various #Published properties and functions to show error.
Is there any possible way to detect changes in child View Model from the ContentView itself?
ContentView
struct ContentView: View {
#State var selectedTab = 0
#StateObject var watchDayProgramVM = WatchDayProgramViewModel()
var body: some View {
NavigationView {
TabView(selection: $selectedTab) {
WatchControlView().id(0)
NowPlayingView().id(1)
}
.environmentObject(watchDayProgramVM)
.onChange(of: self.watchDayProgramVM.workoutModel.currentHeartRate) { newValue in
print("WatchConnectivity heart rate from contentView \(newValue)")
}
}
}
WatchDayProgramViewModel
class WatchDayProgramViewModel: ObservableObject {
#Published var workoutModel = WorkoutModel()
init() {
}
}
WorkoutModel
import Foundation
import HealthKit
class WorkoutModel: NSObject, ObservableObject {
let healthStore = HKHealthStore()
var session: HKWorkoutSession?
var builder: HKLiveWorkoutBuilder?
#Published var currentHeartRate: Double = 0
#Published var workout: HKWorkout?
//Other functions to start/run workout hidden
func updateForStatistics(_ statistics: HKStatistics?) {
guard let statistics = statistics else {
return
}
DispatchQueue.main.async {
switch statistics.quantityType {
case HKQuantityType.quantityType(forIdentifier: .heartRate):
let heartRateUnit = HKUnit.count().unitDivided(by: HKUnit.minute())
self.currentHeartRate = statistics.mostRecentQuantity()?.doubleValue(for: heartRateUnit) ?? 0
default:
return
}
}//end of dispatchqueue
}// end of function
}
extension WorkoutModel: HKLiveWorkoutBuilderDelegate {
func workoutBuilder(_ workoutBuilder: HKLiveWorkoutBuilder, didCollectDataOf collectedTypes: Set<HKSampleType>) {
for type in collectedTypes {
guard let quantityType = type as? HKQuantityType else {
return
}
let statistics = workoutBuilder.statistics(for: quantityType)
updateForStatistics(statistics)
}
}
}
Try to change
#StateObject var watchDayProgramVM = WatchDayProgramViewModel()
with
#ObservedObject var watchDayProgramVM = WatchDayProgramViewModel()
Figure it out. Just had to create another AnyCancellable variable to call objectWillChange publisher.
WatchDayProgramViewModel
class WatchDayProgramViewModel: ObservableObject {
#Published var workoutModel = WorkoutModel()
var cancellable: AnyCancellable?
init() {
cancellable = workoutModel.objectWillChange
.sink { _ in
self.objectWillChange.send()
}
}
}
While I have provided my answer, that worksaround with viewmodels, I would love to see/get advice on other alternatives.

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

SwiftUI ObjectBinding won't receive didchange update from bindable object using combine

I'm testing the Combine framework and using BindableObject as a notification hub for passing data among several views in a SwiftUI ContentView.
One of the views is a table. I click on a row and the value is detected in the print checkpoint, so the bindableobject receives the update.
Problem is, the new string is not broadcasted to the receiving end on the ContentView.
I'm new to this.
View controller with a table view .swift (broadcaster):
import SwiftUI
import Combine
final public class NewestString: BindableObject {
public var didChange = PassthroughSubject<NewestString, Never>()
var newstring: String {
didSet {
didChange.send(self)
print("Newstring: \(newstring)") //<-- Change detected
}
}
init(newstring: String) {
self.newstring = newstring
}
public func update() {
didChange.send(self)
print("--Newstring: \(newstring)")
}
}
final class AViewController: UIViewController {
#IBOutlet weak var someTableView: UITableView!
var returnData = NewestString(newstring:"--")
override func viewDidLoad() {
super.viewDidLoad()
}
}
/// [.....] More extensions here
extension AViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let completion = someResults[indexPath.row]
//// [......] More code here
self.returnData.newstring = "Test string" //<--- change caused
}
}
}
Main content View (broadcast destination):
import SwiftUI
import Combine
struct PrimaryButton: View {
var title: String = "DefaultTitle"
var body: some View {
Button(action: { print("tapped") }) {
Text(title)
}
}
}
struct MyMiniView: View {
#State var aTitle: String = "InitialView"
var body: some View {
VStack{
PrimaryButton(title: aTitle)
}
}
}
struct ContentView: View {
#State private var selection = 0
#ObjectBinding var desiredString: NewestString = NewestString(newstring: "Elegir destino") // <-- Expected receiver
var body: some View {
TabbedView(selection: $selection){
ZStack() {
MyMiniView(aTitle: self.desiredString.newstring ?? "--")
// expected end use of the change, that never happens
[...]
}
struct AView: UIViewControllerRepresentable {
typealias UIViewControllerType = AViewController
func makeUIViewController(context: UIViewControllerRepresentableContext<AView>) -> AViewController {
return UIStoryboard(name: "MyStoryboard", bundle: nil).instantiateViewController(identifier: String(describing: AViewController.self)) as! AViewController
}
func updateUIViewController(_ uiViewController: AViewController, context: UIViewControllerRepresentableContext<AView>) {
//
}
It compiles, runs and prints the change, but no update happens to the MyMiniView's PrimaryButton.
I can't find where you are using your instance of AViewController, but the issue comes from the fact that you are using multiple instance of your bindable object NewestString.
The ContentView as an instance of NewestString, which every update will trigger a view reload.
struct ContentView: View {
#State private var selection = 0
// First instance is here
#ObjectBinding var desiredString: NewestString = NewestString(newstring: "Elegir destino") // <-- Expected receiver
}
The second instance of NewestString is in AViewController, which you actually modify. But, as it's not the same instance of NewestString (the one that is actually declared in the content view), modifying it doesn't trigger the view reload.
final class AViewController: UIViewController {
#IBOutlet weak var someTableView: UITableView!
// The second instance is here
var returnData = NewestString(newstring:"--")
}
To solve this, you need to find a way to "forward" the instance of NewestString created inside your your ContentView to the view controller.
Edit: Found a way to pass the instance of the ObjectBinding to the view controller:
When you add your view into the hierarchy using SwiftUI, you need to pass a Binding of the value that you want to access from the view controller:
struct ContentView: View {
#ObjectBinding var desiredString = NewestString(newstring: "Hello")
var body: some View {
VStack {
AView(binding: desiredString[\.newstring])
Text(desiredString.newstring)
}
}
}
The subscript with a key path will produce a Binding of the given property:
protocol BindableObject {
subscript<T>(keyPath: ReferenceWritableKeyPath<Self, T>) -> > Binding<T> { get }
}
In the view controller wrapper (UIViewControllerRepresentable), you need to forward the given Binding to the actual view controller instance.
struct AView: UIViewControllerRepresentable {
typealias UIViewControllerType = AViewController
var binding: Binding<String>
func makeUIViewController(context: UIViewControllerRepresentableContext<AView>) -> AViewController {
let controller = AViewController()
controller.stringBinding = binding // forward the binding object
return controller
}
func updateUIViewController(_ uiViewController: AViewController, context: UIViewControllerRepresentableContext<AView>) {
}
}
And then, in you view controller, you can use the binding to update your value (using the .value property):
final class AViewController: UIViewController {
var stringBinding: Binding<String>!
override func viewDidLoad() {
super.viewDidLoad()
stringBinding.value = "Hello world !!"
}
}
When the view controller's viewDidLoad is called, the desiredString (in ContentView) will be updated to "Hello world !!", just like the displayed text (Text(desiredString.newstring)).