How to use Bind an Associative Swift enum? - swift

I have a GroupView that accepts a binding as a parameter because I want the GroupView to modify the data in the enum.
Can some help me on how to accomplish this?
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
VStack {
GroupView(group: /* What do i put here? */) // <----------------
}
}
}
struct GroupView: View {
#Binding var group: Group
var body: some View {
Text("Hello World")
}
}
class ViewModel : ObservableObject {
#Published var instruction: Instruction!
init() {
instruction = .group(Group(groupTitle: "A Group struct"))
}
}
enum Instruction {
case group(Group)
}
struct Group { var groupTitle: String }

Well, this certainly will work... but probably there's a better approach to your problem. But no one is in a better position than you, to determine that. So I'll just answer your question about how to pass a binding.
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
VStack {
GroupView(group: viewModel.groupBinding)
}
}
}
class ViewModel : ObservableObject {
#Published var instruction: Instruction!
init() {
instruction = .group(Group(groupTitle: "A Group struct"))
}
var groupBinding: Binding<Group> {
return Binding<Group>(get: {
if case .group(let g) = self.instruction {
return g
} else {
return Group(groupTitle: "")
}
}, set: {
self.instruction = .group($0)
})
}
}

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

Binding #Binding / #State to the #Publisher to decouple VM and View Layer

I want to decouple my ViewModel and View layer to increase testability of my Views.
Therefore, I want to keep my property states inside view and only init them as I needed.
But I cannot initialize my #Binding or #States with #Published properties. Is there a way to couple them inside init function?
I just add example code below to
instead of
import SwiftUI
class ViewModel: ObservableObject {
#Published var str: String = "a"
#Published var int: Int = 1 { didSet { print("ViewModel int = \(int)")} }
init() {
print("ViewModel initialized")
}
}
struct ContentView: View {
#ObservedObject vM = ViewModel()
var body: some View {
Button(action: { vM.int += 1; print(int) }, label: {
Text("Button")
})
}
}
I want to achieve this without using #ObservedObject inside my view.
import SwiftUI
class ViewModel: ObservableObject {
#Published var str: String = "a"
#Published var int: Int = 1 { didSet { print("ViewModel int = \(int)")} }
init() {
print("ViewModel initialized")
}
}
struct ContentView: View {
#Binding var str: String
#Binding var int: Int
var body: some View {
Button(action: { int += 1; print(int) }, label: {
Text("Button")
})
}
}
extension ContentView {
init(viewModel:ObservedObject<ViewModel> = ObservedObject(wrappedValue: ViewModel())) {
// str: Binding<String> and viewModel.str: Published<String>.publisher
// type so that I cannot bind my bindings to viewModel. I must accomplish
// this by using #ObservedObject but this time my view couples with ViewModel
_str = viewModel.wrappedValue.$str
_int = viewModel.wrappedValue.$int
print("ViewCreated")
}
}
// Testing Init
ContentView(str: Binding<String>, int: Binding<Int>)
// ViewModel Init
ContentView(viewModel: ViewModel)
This way I can't bind them each other, I just want to bind my binding or state properties to published properties.
I have realized that by Binding(get:{}, set{}), I can accomplish that. if anyone want to separate their ViewModel and View layer, they can use this approach:
import SwiftUI
class ViewModel: ObservableObject {
#Published var str: String = "a"
#Published var int: Int = 1 { didSet { print("ViewModel int = \(int)")} }
init() {
print("ViewModel initialized")
}
}
struct ContentView: View {
#Binding var str: String
#Binding var int: Int
var body: some View {
Button(action: { int += 1; print(int) }, label: {
Text("Button")
})
}
}
extension ContentView {
init(viewModel:ViewModel = ViewModel()) {
_str = Binding ( get: { viewModel.str }, set: { viewModel.str = $0 } )
_int = Binding ( get: { viewModel.int }, set: { viewModel.int = $0 } )
print("ViewCreated")
}
}

pass down a published var from an generic interactor that conforms to protocol

I am using SomeView in many places with different interactors, so it uses a general interactor that conforms to protocol InteractorProtocol. The problem is SomeView has several SomeButton views which take #Binding as argument and I can't pass down my someState1 and someState2 variables to SomeButton. I could pass my Interactor down to SomeButton and use the interactor variables there, but it feels wrong. Is there a way around this? Could the solution for the interactor maybe be different to make this work?
protocol InteractorProtocol {
var someState1: Bool { get set }
var someState1Published: Published<Bool> { get }
var someState1Publisher: Published<Bool>.Publisher { get }
var someState2: Bool { get set }
var someState2Published: Published<Bool> { get }
var someState2Publisher: Published<Bool>.Publisher { get }
}
class SomeInteractor: ObservableObject & InteractorProtocol {
#Published var someState1 = true
var someState1Published: Published<Bool> { _someState1 }
var someState1Publisher: Published<Bool>.Publisher { $someState1 }
#Published var someState2 = true
var someState2Published: Published<Bool> { _someState2 }
var someState2Publisher: Published<Bool>.Publisher { $someState2 }
}
struct SomeView<Interactor: InteractorProtocol & ObservableObject>: View {
#ObservedObject var interactor: Interactor
var body: some View {
HStack {
SomeButton(selected: self.interactor.$someState1) // not allowed
SomeButton(selected: self.interactor.$someState2) // not allowed
}
}
}
struct SomeButton: View {
#Binding var selected: Bool
var body: some View {
Text("text...")
.background(selected ? Color.green : Color.red)
}
}
The following should work:
protocol InteractorProtocol: ObservableObject {
var someState1: Bool { get set }
// ...
}
class SomeInteractor: InteractorProtocol {
#Published var someState1: Bool = true
// ...
}
struct SomeView<Interactor: InteractorProtocol>: View {
#ObservedObject var interactor: Interactor
var body: some View {
HStack {
SomeButton(selected: self.$interactor.someState1)
}
}
}

What is the correct way to create SwiftUI Binding with array of associative enum?

I got some (unexplained) crashes earlier today, and simplified my code to what is seen below. The crashing went away, but I am not 100% sure. Is the code below the correct way to create Binding on an array of enums? And if yes, can this code be made simpler?
import SwiftUI
enum TheEnum: Hashable {
case one(Int), two(Float)
}
class TestModel : ObservableObject {
#Published var enumArray = [TheEnum.one(5), TheEnum.two(6.0)]
}
struct ContentView: View {
#ObservedObject var testModel = TestModel()
var body: some View {
HStack {
ForEach(testModel.enumArray, id: \.self) { value -> AnyView in
switch value {
case .one(var intVal):
let b = Binding(get: {
intVal
}) {
intVal = $0
}
return AnyView(IntView(intVal: b))
case .two(var floatVal):
let b = Binding(get: {
floatVal
}) {
floatVal = $0
}
return AnyView(FloatView(floatVal: b))
}
}
}
}
}
struct IntView: View {
#Binding var intVal: Int
var body: some View {
Text("\(intVal)")
}
}
struct FloatView: View {
#Binding var floatVal: Float
var body: some View {
Text("\(floatVal)")
}
}

How to pass binding to subview with SwiftUI when the variable is nested in an object?

This works
import SwiftUI
struct ContentView : View {
#State var val1: Int = 0
var body: some View {
MySubview(val1: $val1)
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView(val1: 0)
}
}
#endif
struct MySubview : View {
#Binding var val1: Int
var body: some View {
return Text("Value = \(val1)")
}
}
But when the variable is nested in an Object, this fails
import SwiftUI
struct MyStruct {
let number: Int
}
struct ContentView : View {
#State var val1 = MyStruct(number: 7)
var body: some View {
MySubview(val1: $val1.number)
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView(val1: 0)
}
}
#endif
struct MySubview : View {
#Binding var val1: Int
var body: some View {
return Text("Value = \(val1)")
}
}
Error shown: Generic parameter 'Subject' could not be inferred
How do i pass nested variable as a binding to a subview?
The error is very misleading. Number must be a var, not a let:
struct MyStruct {
var number: Int
}
Change it and it will work fine.
Your code was good except for needing var number: Int as kontiki pointed out.
To help with understanding of passing bindings about between views I prepared the following code which shows use of #Binding in slightly different ways:
import SwiftUI
struct Zoo { var shed: Shed }
struct Shed { var animals: [Animal] }
struct Animal { var legs: Int }
struct ZooView : View {
#State var zoo = Zoo( shed: Shed(animals:
[ Animal(legs: 2), Animal(legs: 4) ] ) )
var body: some View {
VStack {
Text("Legs in the zoo directly:")
Text("Animal 1 Legs: \(zoo.shed.animals[0].legs)")
Text("Animal 2 Legs: \(zoo.shed.animals[1].legs)")
Divider()
Text("And now with nested views:")
ShedView(shed: $zoo.shed)
}
}
}
struct ShedView : View {
#Binding var shed: Shed
var body: some View {
ForEach(shed.animals.indices) { index in
VStack {
Text("Animal: \(index+1)")
AnimalView(animal: self.$shed.animals[index])
}
}
}
}
struct AnimalView : View {
#Binding var animal: Animal
var body: some View {
VStack {
Text("Legs = \(animal.legs)")
Button(
action: { self.animal.legs += 1 }) {
Text("Another leg")
}
}
}
}
In particular ShedView is given a binding to a shed and it looks up an animal in the array of animals in the shed and passes a binding to the animal on to AnimalView.