Pass view to struct in SwiftUI - swift

I'm trying to pass a view into a struct for creating a tab which is the following code:
struct TabItem: TabView {
var tabView: View
var tabText: String
var tabIcon: String
var body: some View {
self.tabView.tabItem {
Text(self.tabText)
Image(systemName: self.tabIcon)
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
TabView {
TabItem(tabView: SimpleCalculatorView(), tabText: "Home", tabIcon: "house")
}
}
}
}
The error I'm getting is the following:
Protocol 'View' can only be used as a generic constraint because it has Self or associated type requirements

The error says here is that the View protocol has associatedtype Body inside it and thus you can't use the View as a type. This means we have to provide more information about the expected type of the tabView. One of the solutions would be to use AnyView as a type, but that would require additional content wrapping into AnyView.
What I would suggest doing here instead of using AnyView is to let the compiler figure out the actual tabView type for you.
Let's tell the compiler that we expect some type Content that confirms to the View protocol
Additionally I don't really see the necessity of using TabView as part of the TabItem declaration. Try going with just View unless you have a strong reason for not doing so.
struct TabItem<Content: View>: View { // Content is a type that we expect. `View` is used instead of `TabView` as it is in original code
var tabView: Content // Using Content instead of a View as it is an actual type now
...
}
The rest of the code can stay unmodified.

Related

SwiftUI ViewBuilder can't pass in different views [duplicate]

This question already has an answer here:
How can I pass 2 content parameters to a SwiftUI View?
(1 answer)
Closed 6 months ago.
In Swift/SwiftUI, I'm trying to make a custom view, which takes other views as arguments. However, I'm getting the error Type of expression is ambiguous without more context.
I know that this error is usually due to mismatching types. Also, in the implementation below, having 2 Text views causes no error, but using a Text view as well as an Image view causes an error.
This makes me believe that the error is due to the fact that Image and Text are technically different types. However, since they both conform to View, and because my CustomView has both parameters of type View, I don't understand why this issue is happening.
struct CustomView<Content: View>: View {
#ViewBuilder var label: Content
#ViewBuilder var content: Content
var body: some View {
VStack {
self.label
self.content
}
}
}
struct ContentView: View {
var body: some View {
CustomView { //<-- ERROR: Type of expression is ambiguous without more context
Image(systemName: "person.3.fill")
} content: {
Text("")
}
}
}
What am I doing wrong, and how can I fix this error? Thanks in advance!
The problem with your code is that in CustomView you defined both label and content as views of a single type (generic Content), but you pass different kinds to the initializer.
One way to fix it is to define different types for label and content:
struct CustomView<Label: View, Content: View>: View {
#ViewBuilder let label: Label
#ViewBuilder let content: Content
// The rest is unchanged.

Why does compiling SwiftUI modifier without leading point compile?

By accident i added a modifier to my swift UI view without a leading point. And it compiled. And i can not wrap my head around why i did that.
Here some example Code:
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(
destination: DetailView(),
label: {
Text("Goto Detail")
})
navigationTitle("ContentView")
}
}
}
struct DetailView: View {
var body: some View {
Text("Detail View")
.padding()
navigationTitle("Detail")
}
}
Somehow even stranger is that the first "wrong" modifier navigationTitle("ContentView") does just nothing.
The second one navigationTitle("Detail") lets the App crash when navigating to the View during runtime.
Similar to this is
struct DetailView: View {
var body: some View {
padding()
}
}
This View does compile but just crashes, if tried to shown with Previews. And it just can't be navigated to.
I would really expect this code to not even compile.
Is somebody able to explain this?
If you refer to a method by its simple name (e.g. navigationTitle), it's kind of like saying self.navigationTitle:
struct DetailView: View {
var body: some View {
Text("Detail View")
.padding()
self.navigationTitle("Detail")
}
}
This is valid, because self is also a View, and that modifier is available on all Views. It gives you a new view that is self, but with a navigation title of Detail. And you are using both of them as the body. Normally you can't return multiple things from a property, but it works here because the body protocol requirement is marked #ViewBuilder.
Of course, you are using self as the self's body, and you can't have self referencing views, so it fails at runtime.
If you add self. to the other cases, it's pretty easy to understand why they compile:
struct DetailView: View {
var body: some View {
self.padding() // self conforms to View, so you can apply padding()
// padding() returns another View, so it's valid to return here
}
}
"The body of DetailView is itself, but with some padding."
NavigationView {
NavigationLink(
destination: DetailView(),
label: {
Text("Goto Detail")
})
self.navigationTitle("ContentView")
}
"The navigation view consists of a navigation link, and ContentView itself (what self means here) with a navigation title of ContentView"
This doesn't crash immediately either because NavigationView's initialiser is smart enough to ignore the junk ContentView that you have given it, or because there is an extra level of indirect-ness to the self-referencing. I don't think we can know exactly why it crashes/doesn't crash immediately, until SwiftUI becomes open source.

SwiftUI creating a dictionary to hold different views

I am experimenting building a pretty simple SwiftUI program to swap views easily. The goal is to make it as simple as possible to add more views without having to change the code that determines which view is how (so a standard if-else isn't going to work).
My current thinking is to keep a dictionary with views stored as part of a key-value pair. A very basic example of the implementation is as follows:
import SwiftUI
struct ViewA: View {
var body: some View {
Text("This is View A")
}
}
struct ViewB: View {
var body: some View {
Text("This is View B")
}
}
struct MainView: View {
var subviews: [String:View] = [
"View-1": ViewA(),
"View-2": ViewB(),
]
var body: some View {
self.subviews["View-1"]
}
}
I am however getting an error on the lines where I am creating the dictionary: Protocol 'View' can only be used as a generic constraint because it has Self or associated type requirements.
I have tried a number of different types for the value part of the dictionary, including AnyView, Groups, and making a generic type <Content: View> on the MainView struct. These however give more/different errors.
I have looked at the project SwiftUIRouter, as it kind of solves what I a trying to achieve, however my goals are a bit more simplistic.
Thanks in advance.
Swift doesn't have named subscripts, so you have to put it together hackily, but you can use the same calling syntax with otherwise useful/better language features:
struct MainView: View {
var body: some View {
subviews["View-1"]
}
enum subviews {
#ViewBuilder static subscript(string: String) -> some View {
switch string {
case "View-1":
ViewA()
case "View-2":
ViewB()
default:
fatalError()
}
}
}
}

SwiftUI - #Binding to a computed property which accesses value inside ObservableObject property duplicates the variable?

In the code below (a stripped-down version of some code in a project) I'm using a MVVM pattern with two views:
ViewA - displays a value stored in an ObservableObject ViewModel;
ViewB - displays the same value and has a Slider that changes that value, which is passed to the view using Binding.
Inside of ViewModelA I have a computed property which serves both to avoid the View from accessing the Model directly and to perform some other operations when the value inside the model (the one being displayed) is changed.
I'm also passing that computed value to a ViewModelB, using Binding, which acts as a StateObject for ViewB. However, when dragging the Slider to change that value, the value changes on ViewA but doesn't change on ViewB and the slider itself doesn't slide. As expected, when debugging, the wrappedValue inside the Binding is not changing. But how is the change propagated upwards (through the Binding's setters, I imagine) but not downwards back to ViewB?? I imagine this can only happen if the variable is being duplicated somewhere and changed only in one place, but I can't seem to understand where or if that's what's actually happening.
Thanks in advance!
Views:
import SwiftUI
struct ContentView: View {
#StateObject var viewModelA = ViewModelA()
var body: some View {
VStack{
ViewA(value: viewModelA.value)
ViewB(value: $viewModelA.value)
}
}
}
struct ViewA: View {
let value: Double
var body: some View {
Text("\(value)").padding()
}
}
struct ViewB: View {
#StateObject var viewModelB: ViewModelB
init(value: Binding<Double>){
_viewModelB = StateObject(wrappedValue: ViewModelB(value: value))
}
var body: some View {
VStack{
Text("\(viewModelB.value)")
Slider(value: $viewModelB.value, in: 0...1)
}
}
}
ViewModels:
class ViewModelA: ObservableObject {
#Published var model = Model()
var value: Double {
get {
model.value
}
set {
model.value = newValue
// perform other checks and operations
}
}
}
class ViewModelB: ObservableObject {
#Binding var value: Double
init(value: Binding<Double>){
self._value = value
}
}
Model:
struct Model {
var value: Double = 0
}
If you only look where you can't go, you might just miss the riches below
Breaking single source of truth, and breaching local (private) property of #StateObjectby sharing it via Binding are two places where you can't go.
#EnvironmentObject or more generally the concept of "shared object" between views are the riches below.
This is an example of doing it without MVVM nonsense:
import SwiftUI
final class EnvState: ObservableObject {#Published var value: Double = 0 }
struct ContentView: View {
#EnvironmentObject var eos: EnvState
var body: some View {
VStack{
ViewA()
ViewB()
}
}
}
struct ViewA: View {
#EnvironmentObject var eos: EnvState
var body: some View {
Text("\(eos.value)").padding()
}
}
struct ViewB: View {
#EnvironmentObject var eos: EnvState
var body: some View {
VStack{
Text("\(eos.value)")
Slider(value: $eos.value, in: 0...1)
}
}
}
Isn't this easier to read, cleaner, less error-prone, with fewer overheads, and without serious violation of fundamental coding principles?
MVVM does not take value type into consideration. And the reason Swift introduces value type is so that you don't pass shared mutable references and create all kinds of bugs.
Yet the first thing MVVM devs do is to introduce shared mutable references for every view and pass references around via binding...
Now to your question:
the only options I see are either using only one ViewModel per Model, or having to pass the Model (or it's properties) between ViewModels through Binding
Another option is to drop MVVM, get rid of all view models, and use #EnvironmentObject instead.
Or if you don't want to drop MVVM, pass #ObservedObject (your view model being a reference type) instead of #Binding.
E.g.;
struct ContentView: View {
#ObservedObject var viewModelA = ViewModelA()
var body: some View {
VStack{
ViewA(value: viewModelA)
ViewB(value: viewModelA)
}
}
}
On a side note, what's the point of "don't access model directly from view"?
It makes zero sense when your model is value type.
Especially when you pass view model reference around like cookies in a party so everyone can have it.
Really it looks like broken single-source or truth concept. Instead the following just works (ViewModelB might probably be needed for something, but not for this case)
Tested with Xcode 12 / iOS 14
Only modified parts:
struct ContentView: View {
#StateObject var viewModelA = ViewModelA()
var body: some View {
VStack{
ViewA(value: viewModelA.value)
ViewB(value: $viewModelA.model.value)
}
}
}
struct ViewB: View {
#Binding var value: Double
var body: some View {
VStack{
Text("\(value)")
Slider(value: $value, in: 0...1)
}
}
}

How do I fix the "Function declares an opaque return type, but has no return statements in its body from which to infer an underlying type" error?

struct ContentView: View {
var body: some View {
Print("Hello World")
}
}
Whenever I try using things like print's or if statements or anything like that (Like using UserDefaults for saving settings for another example) outside of an action then it gives me the "Function declares an opaque return type", but has no return statements in its body from which to infer an underlying type".
With UIKit whatever thing like that you put in the viewDidLoad() would run perfectly so is there something like viewDidLoad() for SwiftUI ? If not how would I go about performing these actions (and before you say taking them out of the body and putting them in the ContentView that'll just give me the "Expected declaration" error)?
As mentioned, the body of the view, must return a View:
struct ContentView : View {
var body: some View {
return Text("Hello World")
}
}
Additionally, a large amount of restrictions are in place when defining the body.
There's no way around it. You need to read the documentation, or at least follow these tutorials from Apple, which are very well put together:
https://developer.apple.com/tutorials/swiftui/tutorials
import SwiftUI
struct ContentView : View {
var body: some View {
var bodyView: some View {
Text("Hello World")
}
//Put your entire UI in the bodyView, between the bodyView and "return bodyView" you can put what you would as if it was a viewDidLoad() like in UIKit.
return bodyView
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif