Mutable Binding in Swiftui Live Preview

Mutable Binding in SwiftUI Live Preview

Updates to a @State variable in a PreviewProvider appear to not update the the read-only computed property previews directly. The solution is to wrap the @State variable in a test holder view. Then use this test view inside the previews property so the Live Preview refreshes correctly. Tested and working in Xcode 11.2.1.

struct ChildView: View {
@Binding var itemName: String

var body: some View {
VStack {
Text("Name: \(itemName)")
Button(action: {
self.itemName = "different value"
}) {
Text("Change")
}
}
}
}

struct ChildView_Previews: PreviewProvider {

struct BindingTestHolder: View {
@State var testItem: String = "Initial"
var body: some View {
ChildView(itemName: $testItem)
}
}

static var previews: some View {
BindingTestHolder()
}
}

SwiftUI preview page, bindable URL?

You can create a @State variable that will be fed into the userInfo like this.

struct AdminView_Previews: PreviewProvider {

@State var mockUserInfo: UserModel = UserModel(username: "dm1886",
email: "dsadsa@gmail.com",
userID: "test",
adminLevel: "user",
immagine: nil)

static var previews: some View {
Group {
NavigationView{
AdminView(dm: DataManager(), userInfo: $mockUserInfo)
}

NavigationView{
AdminView(dm: DataManager(), userInfo: $mockUserInfo)
}
.previewDisplayName("Test")
.background(Color(.systemBackground))
.environment(\.colorScheme, .dark)
}
}
}

Can you share the code of your AdminView as well? The variable immagine might not be working because you need to handle the case for the URL? inside the AdminView. I don't know what's inside your AdminView, but handling the path for when immagine is nil to return an EmptyView could to the trick.

SwiftUI @Binding Initialize

When you use your LoggedInView in your app you do need to provide some binding, such as an @State from a previous view or an @EnvironmentObject.

For the special case of the PreviewProvider where you just need a fixed value you can use .constant(false)

E.g.

#if DEBUG
struct LoggedInView_Previews : PreviewProvider {
static var previews: some View {
LoggedInView(dismissView: .constant(false))
}
}
#endif

Toggling a Binding Bool with a SwiftUI button action

You need to provide mutable state, not a constant value. For example like this.

import SwiftUI

struct LabeledCheckbox: View {
var labelText: String
@Binding var isChecked: Bool

var checkBox: Image {
Image(systemName: isChecked ? "checkmark.circle" : "circle")
}

var body: some View {
Button(action: {
self.isChecked.toggle()
}) {
HStack {
checkBox
Text(labelText)
}
}
}
}

struct ContentView: View {
@State private var isChecked = false

var body: some View {
LabeledCheckbox(labelText: "Checkbox", isChecked: $isChecked)
}
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Accessing Value of SwiftUI View through a UIKit View

We can decorate the RatingView and use an ObservableObject to hold on to the source of truth.

class RatingObserver: ObservableObject {
@Published var rating: Int?
}

struct WrappedRatingView: View {

@ObservedObject var ratingObserver: RatingObserver

var body: some View {
RatingView(rating: $ratingObserver.rating)
}
}

Then we can use it in the following way.

class ViewController: UIViewController {

let ratingObserver = RatingObserver()

override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white

let hostingController = UIHostingController(
rootView: WrappedRatingView(ratingObserver: ratingObserver)
)
self.addChild(hostingController)
view.addSubview(hostingController.view)
hostingController.didMove(toParent: self)
hostingController.view.translatesAutoresizingMaskIntoConstraints = false

let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Reset Rating", for: .normal)
button.setTitleColor(.blue, for: .normal)
button.addTarget(self, action: #selector(resetRating), for: .touchUpInside)
view.addSubview(button)

NSLayoutConstraint.activate([
hostingController.view.centerXAnchor.constraint(equalTo: view.centerXAnchor),
hostingController.view.centerYAnchor.constraint(equalTo: view.centerYAnchor),
button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
button.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 100)
])
}

@objc func resetRating() {
ratingObserver.rating = nil
}
}

This allows for updating both the ViewController and the SwiftUI view.

SwiftUI: How to implement a custom init with @Binding variables

Argh! You were so close. This is how you do it. You missed a dollar sign (beta 3) or underscore (beta 4), and either self in front of your amount property, or .value after the amount parameter. All these options work:

You'll see that I removed the @State in includeDecimal, check the explanation at the end.

This is using the property (put self in front of it):

struct AmountView : View {
@Binding var amount: Double

private var includeDecimal = false

init(amount: Binding<Double>) {

// self.$amount = amount // beta 3
self._amount = amount // beta 4

self.includeDecimal = round(self.amount)-self.amount > 0
}
}

or using .value after (but without self, because you are using the passed parameter, not the struct's property):

struct AmountView : View {
@Binding var amount: Double

private var includeDecimal = false

init(amount: Binding<Double>) {
// self.$amount = amount // beta 3
self._amount = amount // beta 4

self.includeDecimal = round(amount.value)-amount.value > 0
}
}

This is the same, but we use different names for the parameter (withAmount) and the property (amount), so you clearly see when you are using each.

struct AmountView : View {
@Binding var amount: Double

private var includeDecimal = false

init(withAmount: Binding<Double>) {
// self.$amount = withAmount // beta 3
self._amount = withAmount // beta 4

self.includeDecimal = round(self.amount)-self.amount > 0
}
}
struct AmountView : View {
@Binding var amount: Double

private var includeDecimal = false

init(withAmount: Binding<Double>) {
// self.$amount = withAmount // beta 3
self._amount = withAmount // beta 4

self.includeDecimal = round(withAmount.value)-withAmount.value > 0
}
}

Note that .value is not necessary with the property, thanks to the property wrapper (@Binding), which creates the accessors that makes the .value unnecessary. However, with the parameter, there is not such thing and you have to do it explicitly. If you would like to learn more about property wrappers, check the WWDC session 415 - Modern Swift API Design and jump to 23:12.

As you discovered, modifying the @State variable from the initilizer will throw the following error: Thread 1: Fatal error: Accessing State outside View.body. To avoid it, you should either remove the @State. Which makes sense because includeDecimal is not a source of truth. Its value is derived from amount. By removing @State, however, includeDecimal will not update if amount changes. To achieve that, the best option, is to define your includeDecimal as a computed property, so that its value is derived from the source of truth (amount). This way, whenever the amount changes, your includeDecimal does too. If your view depends on includeDecimal, it should update when it changes:

struct AmountView : View {
@Binding var amount: Double

private var includeDecimal: Bool {
return round(amount)-amount > 0
}

init(withAmount: Binding<Double>) {
self.$amount = withAmount
}

var body: some View { ... }
}

As indicated by rob mayoff, you can also use $$varName (beta 3), or _varName (beta4) to initialise a State variable:

// Beta 3:
$$includeDecimal = State(initialValue: (round(amount.value) - amount.value) != 0)

// Beta 4:
_includeDecimal = State(initialValue: (round(amount.value) - amount.value) != 0)

Delete a Binding from a list in SwiftUI

This happens because you're enumerating by indices and referencing binding by index inside ForEach

I suggest you switching to ForEachIndexed: this wrapper will pass both index and a correct binding to your block:

struct ForEachIndexed<Data: MutableCollection&RandomAccessCollection, RowContent: View, ID: Hashable>: View, DynamicViewContent where Data.Index : Hashable
{
var data: [(Data.Index, Data.Element)] {
forEach.data
}

let forEach: ForEach<[(Data.Index, Data.Element)], ID, RowContent>

init(_ data: Binding<Data>,
@ViewBuilder rowContent: @escaping (Data.Index, Binding<Data.Element>) -> RowContent
) where Data.Element: Identifiable, Data.Element.ID == ID {
forEach = ForEach(
Array(zip(data.wrappedValue.indices, data.wrappedValue)),
id: \.1.id
) { i, _ in
rowContent(i, Binding(get: { data.wrappedValue[i] }, set: { data.wrappedValue[i] = $0 }))
}
}

init(_ data: Binding<Data>,
id: KeyPath<Data.Element, ID>,
@ViewBuilder rowContent: @escaping (Data.Index, Binding<Data.Element>) -> RowContent
) {
forEach = ForEach(
Array(zip(data.wrappedValue.indices, data.wrappedValue)),
id: (\.1 as KeyPath<(Data.Index, Data.Element), Data.Element>).appending(path: id)
) { i, _ in
rowContent(i, Binding(get: { data.wrappedValue[i] }, set: { data.wrappedValue[i] = $0 }))
}
}

var body: some View {
forEach
}
}

Usage:

ForEachIndexed($todoViewModel.todos) { index, todoBinding in
TextField("Test", text: todoBinding.title)
.contextMenu(ContextMenu(menuItems: {
VStack {
Button(action: {
self.todoViewModel.deleteAt(index)
}, label: {
Label("Delete", systemImage: "trash")
})
}
}))
}

Binding and State not updating at the same time

structs are immutable when you update a variable that has a SwiftUI wrapper you are telling it so reload the entire View/struct. So, when you update x1 it triggers a reload and when you update x2 you trigger another.

If you want to control when the View does a reload you have to use variables that are mutable/live in a class, and are not wrapped with a SwiftUI wrapper.

struct OtherView: View {
let size: CGSize
@EnvironmentObject var vm: SyncUpdateViewModel
//@Binding var x1: Int
@State var x2: Int = 0

var body: some View {
Text(text())
.frame(width: size.width,
height: size.height / 2)
.background(Color.blue)
.onTapGesture {
print("*** TAP ***")
withAnimation(Animation.easeIn(duration: 4)) {
vm.x1 += 1
x2 += 1
}

}
}

func text() -> String {
print("x1: \(vm.x1) + x2 = \(x2) = \(vm.x1 + x2)")
return "x1: \(vm.x1) + x2 = \(x2) = \(vm.x1 + x2)"
}
}
class SyncUpdateViewModel: ObservableObject {
var x1: Int = 0
}
struct SyncUpdate: View {
@StateObject var vm: SyncUpdateViewModel = SyncUpdateViewModel()
//@State private var value: Int = 0
var body: some View {
NavigationView {
GeometryReader { proxy in
OtherView(size: proxy.size).environmentObject(vm)
}
}
}

}



Related Topics



Leave a reply



Submit