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
struct
s 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
Add Animations to Foreach Loop Elements (Swiftui)
Set the Size and Position of All Windows on the Screen in Swift
Adding Local Dependencies in Xcode11 Using Spm
Autolayout Contraints for a View from Xib
Find Nearest Smaller Number in Array
How to Detect Vertical Planes in Arkit
Prefer Large Titles and Refreshcontrol Not Working Well
Differencebetween Http Parameters and Http Headers
How to Include Assets/Resources in a Swift Package Manager Library
Get Image from Calayer or Nsview (Swift 3)
App Delegate Accessing Environment Object
Create Nsmanagedobject Subclass... Make a New Error in My Project
Count Elements of Array Matching Condition in Swift
Uicollectionviewlayout Not Working with Uiimage in Swift 5 and Xcode 11
Multiple Bottom Sheets - the Content Doesn't Load Swiftui