How to create a simple Binding for previews - swiftui

With the new #Binding delegate and previews, I find it kinda awkward to always have to create a #State static var to create the neccesarry binding:
struct TestView: View {
#Binding var someProperty: Double
var body: some View {
//...
}
}
#if DEBUG
struct TestView_Previews : PreviewProvider {
#State static var someProperty = 0.7
static var previews: some View {
TestView(someProperty: $someProperty)
}
}
#endif
Is there a simpler way to create a binding, that proxies a simple values for testing and previewing?

You can use .constant(VALUE) in your Previews, no need to create a #State.
/// A value and a means to mutate it.
#propertyWrapper public struct Binding<Value> {
/// Creates a binding with an immutable `value`.
public static func constant(_ value: Value) -> Binding<Value>
}
e.g.
TestView(someProperty: .constant(5.0))

I wrote about this in depth here, but the short version is simply to add the following extension:
extension Binding {
public static func variable(_ value: Value) -> Binding<Value> {
var state = value
return Binding<Value> {
state
} set: {
state = $0
}
}
}
And then do...
struct TestView_Previews : PreviewProvider {
static var previews: some View {
TestView(someProperty: .variable(0.7)
}
}

Related

SwiftUI re-initialize EnvironmentObject?

How can I refresh an environment var in SwiftUI? It is easy to update any object that's a part of an environment object, but it seems like there should be a way to re-initialize.
struct reinitenviron: View{
#EnvironmentObject private var globalObj: GlobalClass
var body: some View{
Text("refresh").onTapGesture {
globalObj = GlobalClass() //error here
}
}
}
The following gives an error that globalObj is get only. Is it possible to re-initialize?
A possible solution is to introduce explicit method in GlobalClass to reset it to initial state and use that method and in init and externally, like
class GlobalClass: ObservableObject {
#Published var value: Int = 1
init() {
self.reset()
}
func reset() {
self.value = 1
// do other activity if needed
}
}
struct reinitenviron: View{
#EnvironmentObject private var globalObj: GlobalClass
var body: some View{
Text("refresh").onTapGesture {
globalObj.reset() // << here
}
}
}

Binding Constant Alternative, but Mutable

I have a view like this:
struct View1: View {
#Binding var myVariable: Bool
init() {
_myVariable = Binding.constant(true) // It works but myVariable is immutable, so I can't edit myVariable
}
init(myVariable: Binding<Bool>) {
_myVariable = myVariable
}
var body: some View {
Button("Change") {
myVariable.toggle()
}
}
}
struct View2: View {
var body: some View {
View1()
}
}
struct View3: View {
#State var myVariable = false
var body: some View {
View1(myVariable: $myVariable)
}
}
And I want to make this: If there is a parameter provided, set this to myVariable like second init in View1. Else, set the first value of myVariable like in first init.
I tried to use Binding.constant(value) but it is immutable. And I can't edit the variable. So, I need a mutable Binding initializer like Binding.constant(value). But I can't find it.
How can I solve this problem?
To avoid over-complicating View1, you can create an intermediate view with that name, and then have a private 'internal' view which has the actual implementation.
Code:
private struct View1Internal: View {
#Binding var myVariable: Bool
var body: some View {
Button("Change") {
myVariable.toggle()
}
}
}
struct View1: View {
private enum Kind {
case state
case binding(Binding<Bool>)
}
#State private var state = true
private let kind: Kind
init() {
kind = .state
}
init(myVariable: Binding<Bool>) {
kind = .binding(myVariable)
}
var body: some View {
switch kind {
case .state: View1Internal(myVariable: $state)
case .binding(let binding): View1Internal(myVariable: binding)
}
}
}

SwiftUI: #Environment not receiving provided value down the view hierarchy

I am following the example of this project to create my iOS app (thanks Alexey!), but can't get the #Environment variable to receive the value that is being passed down the UI hierarchy. The top level view receives the correct value, but the downstream view receives the default value.
EDIT: After tying to replicate Asperi's code, I found that this behavior happens only when the downstream view is invoked via a NavigationLink. Updated the code below:
EDIT2: The problem was with where the environment method was being invoked. Invoking it on the NavigationView instead of the MainView solved the problem. Code updated below:
Custom Environment key - DIContainer
struct DIContainer: EnvironmentKey {
let interactor: Interactor
init(interactor: Interactor) {
self.interactor = interactor
}
static var defaultValue: Self { Self.default }
private static let `default` = Self(interactor: .stub)
}
extension EnvironmentValues {
var injected: DIContainer {
get { self[DIContainer.self] }
set { self[DIContainer.self] = newValue }
}
}
App struct
private let container: DIContainer
init() {
container = DIContainer(interactor: RealInteractor())
}
var body: some Scene {
WindowGroup {
NavigationView {
MainView()
}
.environment(\.injected, container)
}
Main View
struct MainView: View {
#Environment(\.injected) private var injected: DIContainer
// `injected` has the `RealInteractor`, as expected
var body: some View {
VStack {
Text("Main: \(injected.foo())") \\ << Prints REAL
NavigationLink(destination: SearchView()) {
Text("Search")
}
}
}
}
Search View
struct SearchView: View {
#Environment(\.injected) private var injected: DIContainer
// `injected` has the `StubInteractor`, why?
var body: some View {
Text("Search: \(injected.foo())")
}
}
I am able to solve this problem by modifying the MainView like so:
var body: some View {
SearchView()
.environment(\.injected, container)
}
But isn't avoiding doing this repeatedly the purpose of #Environment?
Any guidance/pointers appreciated.
I've tryied to replicate all parts and to make them compiled... and the result just works as expected - environment is passed down the view hierarchy, so you might miss something in your real code.
Here is complete module, tested with Xcode 12.4 / iOS 14.4
class Interactor { // << replicated !!
static let stub = Interactor()
func foo() -> String { "stub" }
}
class RealInteractor: Interactor { // << replicated !!
override func foo() -> String { "real" }
}
struct ContentView: View { // << replicated !!
private let container: DIContainer
init() {
container = DIContainer(interactor: RealInteractor())
}
var body: some View {
NavigationView {
MainView()
}
.environment(\.injected, container) // << to affect any links !!
}
}
// no changes in env parts
struct DIContainer: EnvironmentKey {
let interactor: Interactor
init(interactor: Interactor) {
self.interactor = interactor
}
static var defaultValue: Self { Self.default }
private static let `default` = Self(interactor: .stub)
}
extension EnvironmentValues {
var injected: DIContainer {
get { self[DIContainer.self] }
set { self[DIContainer.self] = newValue }
}
}
struct MainView: View {
#Environment(\.injected) private var injected: DIContainer
// `injected` has the `RealInteractor`, as expected
var body: some View {
SearchView()
}
}
// just tested here
struct SearchView: View {
#Environment(\.injected) private var injected: DIContainer
var body: some View {
Text("Result: \(injected.interactor.foo())") // << works fine !!
}
}

How do you preview a view containing a binding to its parent view's state?

I present this view as a sheet from its parent view
struct NamesView: View {
#Binding var match: Match
var body: some View {
...
}
}
Since the match source of truth is in the parent view presenting this NamesView sheet, when the view is constructed I pass in a $match binding and data flows as intended.
However, when constructing this view in a preview provider
struct NamesView_Previews: PreviewProvider {
static var previews: some View {
NamesView()
}
}
the compiler says that NamesView() expects a match argument of type Binding<Match> (Match being the parent view presenting this view as a sheet). I'm not sure what would be a good way to proceed from here or if this is a limitation of SwiftUI.
If you want only constant preview, then it can be
struct NamesView_Previews: PreviewProvider {
static var previews: some View {
NamesView(match: .constant(Match()))
}
}
if you want it in live, the it can be
struct NamesView_Previews: PreviewProvider {
struct BindingTestHolder: View {
#State private var testedMatch = Match()
var body: some View {
NamesView(match: $testedMatch)
}
}
static var previews: some View {
BindingTestHolder()
}
}
Try this:
struct NamesView_Previews: PreviewProvider {
static var previews: some View {
NamesView(match:.constant(Match()))
}
}
I wrote about this in depth here, but the short version is simply to add the following extension:
extension Binding {
public static func variable(_ value: Value) -> Binding<Value> {
var state = value
return Binding<Value> {
state
} set: {
state = $0
}
}
}
And then do...
struct NamesView_Previews : PreviewProvider {
static var previews: some View {
NamesView(match: .variable(Match()))
}
}
This lets you actually mutate the value, useful when doing live previews in Xcode 14.

How to initialize a Property from one struct in an other struct

I was looking for some Code (SwiftUI) to get a JSON File from a website into my Xcode Project, and I found a good Sample but when I try to change the Code and wanna use a 'var' from one struct in an other struct.
struct Course: Decodable, Identifiable, Hashable {
let id: Int
let name: String
var link: String
let imageUrl: String
let number_of_lessons: Int
}
class NetworkManager: ObservableObject {
#Published var courses = [Course]()
func getAllCourses() {
guard let url = URL(string: "https://api.letsbuildthatapp.com/jsondecodable/courses") else { return }
URLSession.shared.dataTask(with: url) { data, response, error in
do {
let courses = try JSONDecoder().decode([Course].self, from: data!)
DispatchQueue.main.async {
self.courses = courses
print(courses)
}
} catch {
print("Failed To decode: ", error)
}
}.resume()
}
init() {
getAllCourses()
}
}
struct SwiftUIView: View {
var cs:Course
var body: some View {
Text(cs.name)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
SwiftUIView() //here it says Missing argument for parameter...
}
Xcode says: Missing argument for parameter 'cs' in call, Insert 'cs:<#Course#>'.
As you are trying to initialize a struct, you need to pass all properties with setters, here that is cs, as body is a get-only property.
You will need to pass a Course object via SwiftUIView(cs: course). This course can be just a static course, as it's only used in your SwiftUI preview.
Swift autogenerates initializers for structs, so your SwiftUIView has one like this:
public init(cs: Course) {
self.cs = cs
}
So in order to create an instance of your SwiftUIView in the previews you have to declare it like this:
struct ContentView_Previews: PreviewProvider {
static let cs = Course(id: 7,
name: "The best course",
link: "thebestcourse.com",
imageUrl: "thebestcourse.com/image.jpg",
number_of_lessons: 5)
static var previews: some View {
SwiftUIView(cs: cs)
}
}