Do I have to downcast or am I missing something? - swift

So I have a Type that I would like to make dynamic at the call site:
struct DynamicView<Content: View>: View {
let content: Content
var body: some View {
content
}
}
using some type of function that would convert it into something concrete
static func contentCreator<T: View>(id: Int) -> T {
switch id {
case 0:
return FirstView() //<-- Xcode is demanding that I state as! T here
}
}
where FirstView
struct FirstView: View {
var body: some View {
Circle()
}
}
Am I approaching this wrong or what? As long as they all conform to a common protocol, they should be interchangeable right?

That's because the type is not inferred by checking return.
If you try:
let v = contentCreator(id: 0)
You will get a Generic parameter T could not be inferred error, because v type is unknown.
If type is set, it will compile :
let v: View = contentCreator(id: 0)
So to get back to you interrogation, since View is a protocol, when Xcode builds the source, it has no way to know the class 'T', and no way to force you to use the right type when you use it.
struct FirstView: View {
var body: some View {
Circle()
}
}
struct SecondView: View {
var body: some View {
Square()
}
}
// This will work, because once executed, we know v1 class.
// The cast will work as long as v1 is declared as conforming to 'View' protocol.
// Type is not inferred in this case, but known only when it returns.
let v1: View = contentCreator(0)
/// This won't work, because the inferred type is 'SecondView'.
/// The cast 'return FirstView() as! T' will crash
let v2: SecondView = contentCreator(0)

I assume you just looking for something like this
#ViewBuilder
func contentCreator(id: Int) -> some View {
switch id {
case 0:
FirstView()
case 1:
SecondView()
// ...
default:
EmptyView()
}
}

Related

How to pass view of different type to another view in SwiftUI?

So I have this TestView which accepts headerContent and bodyContent,
struct TestView<Content: View>: View {
var headerContent: (() -> Content)? = nil
let bodyContent: () -> Content
var body: some View {
VStack {
headerContent?()
bodyContent()
}
}
}
And I use it as,
struct ContentView: View {
var body: some View {
TestView(headerContent: {
Text("HeaderContent")
}) {
ScrollView {
}
}
}
}
But I get the following error,
Cannot convert value of type 'ScrollView<EmptyView>' to closure result type 'Text'
What am I missing?
You need to have two View generics, since headerContent and bodyContent are not the same.
Without this, you are saying there is some concrete type Content that conforms to View. However, both Text and ScrollView are different types, not the same.
Code:
struct TestView<HeaderContent: View, BodyContent: View>: View {
var headerContent: (() -> HeaderContent)? = nil
let bodyContent: () -> BodyContent
var body: some View {
VStack {
headerContent?()
bodyContent()
}
}
}

SwiftUI - Can I define an empty initializer for a generic view?

I have a generic view with an optional #ViewBuilder.
I want to have two initializers, one is responsible for setting the #ViewBuilder and another which should serve as an empty default.
struct OptionalViewBuilder<Content: View>: View {
let content: (() -> Content)?
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
init() {
self.content = nil
}
var body: some View {
VStack {
Text("Headline")
Divider()
content?()
}
}
}
If I initialize the view with the ViewBuilder parameter, it works of course. However, when I use the empty initializer, I get the error message:
Generic parameter 'Content' could not be inferred
struct ContentView: View {
var body: some View {
OptionalViewBuilder {
Text("Text1")
Text("Text2")
}
OptionalViewBuilder() // <-- Generic parameter 'Content' could not be inferred
}
}
I know that Swift needs explicit types at runtime, but is there a way to make the default initializer work anyway?
Because OptionalViewBuilder is generic with respect to its Content type, you'd need to define that type as something in your default case. This could be EmptyView, if you don't want to render anything.
To do that, you'd need an init which is constrained to Content == EmptyView, and the content property need not be optional:
struct OptionalViewBuilder<Content: View>: View {
let content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
init() where Content == EmptyView {
self.init(content: { EmptyView() })
}
var body: some View {
VStack {
Text("Headline")
Divider()
content()
}
}
}
I can make the compile error go away by adding an empty closure, like this:
struct ContentView: View {
var body: some View {
OptionalViewBuilder {
Text("Text1")
Text("Text2")
}
OptionalViewBuilder() {} // I added an empty closure
}
}
Does this get you what you need?

How to create a struct parameter that is a closure with input, returning `some View` instead of `AnyView` in SwiftUI?

Current situation:
I'm trying to create a reusable view in SwiftUI that returns another view passed into it. At the moment I'm using a closure that returns AnyView, as the parameter.
When I initialize the reusable view I pass on a custom view wrapped inside AnyView().
Goal:
Ideally I'd like a way to omit the AnyView() wrapper and just pass on my CustomViewA or CustomViewB as is.
My attempts and issues:
I tried changing the closure parameter to return some View but I'm getting the following error:
'some' types are only implemented for the declared type of properties and subscripts and the return type of functions
I've seen a few attempts on S.O. to work around that by somehow using a generic parameter but I'm not sure how to implement this in my case or if it's even possible. Also important to note, the closure needs to take a String as its own input parameter.
Example code:
import SwiftUI
struct ReusableView: View {
let outputView: (String) -> AnyView
var body: some View {
outputView("Hello World")
}
}
struct CustomViewA: View {
let label: String
init(_ label: String) {
self.label = label
}
var body: some View {
return Text(label)
}
}
struct CustomViewB: View {
let label: String
init(_ label: String) {
self.label = label
}
var body: some View {
return HStack{
Text(label)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ReusableView() {
AnyView(CustomViewA($0))
}
}
}
You just need to make ReusableView generic and declare the return type of your closure as the generic type.
struct ReusableView<Output: View>: View {
let outputView: (String) -> Output
var body: some View {
outputView("Hello World")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ReusableView() {
CustomViewA($0)
}
}
}

Binding to a read-only property in SwiftUI

I have a model type which looks like this:
enum State {
case loading
case loaded([String])
case failed(Error)
var strings: [String]? {
switch self {
case .loaded(let strings): return strings
default: return nil
}
}
}
class MyApi: ObservableObject {
private(set) var state: State = .loading
func fetch() {
... some time later ...
self.state = .loaded(["Hello", "World"])
}
}
and I'm trying to use this to drive a SwiftUI View.
struct WordListView: View {
#EnvironmentObject var api: MyApi
var body: some View {
ZStack {
List($api.state.strings) {
Text($0)
}
}
}
}
It's about here that my assumptions fail. I'm trying to get a list of the strings to render in my List when they are loaded, but it won't compile.
The compiler error is Generic parameter 'Subject' could not be inferred, which after a bit of googling tells me that bindings are two-way, so won't work with both my private(set) and the var on the State enum being read-only.
This doesn't seem to make any sense - there is no way that the view should be able to tell the api whether or not it's loading, that definitely should be a one-way data flow!
I guess my question is either
Is there a way to get a one-way binding in SwiftUI - i.e. some of the UI will update based on a value it cannot change.
or
How should I have architected this code! It's very likely that I'm writing code in a style which doesn't work with SwiftUI, but all the tutorials I can see online neatly ignore things like loading / error states.
You don't actually need a binding for this.
An intuitive way to decide if you need a binding or not is to ask:
Does this view need to modify the passed value ?
In your case the answer is no. The List doesn't need to modify api.state (as opposed to a textfield or a slider for example), it just needs the current value of it at any given moment. That is what #State is for but since the state is not something that belongs to the view (remember, Apple says that each state must be private to the view) you're correctly using some form of an ObservableObject (through Environment).
The final missing piece is to mark any of your properties that should trigger an update with #Published, which is a convenience to fire objectWillChange signals and instruct any observing view to recalculate its body.
So, something like this will get things done:
class MyApi: ObservableObject {
#Published private(set) var state: State = .loading
func fetch() {
self.state = .loaded(["Hello", "World"])
}
}
struct WordListView: View {
#EnvironmentObject var api: MyApi
var body: some View {
ZStack {
List(api.state.strings ?? [], id: \.self) {
Text($0)
}
}
}
}
Not exactly the same problem as I had, but the following direction can help you possibly find a good result when bindings are done with only reads.
You can create a custom binding using a computed property.
I needed to do exactly this in order to show an alert only when one was passed into an overlay.
Code looks something along these lines :
struct AlertState {
var title: String
}
class AlertModel: ObservableObject {
// Pass a binding to an alert state that can be changed at
// any time.
#Published var alertState: AlertState? = nil
#Published var showAlert: Bool = false
init(alertState: AnyPublisher<AlertState?, Never>) {
alertState
.assign(to: &$alertState)
alertState
.map { $0 != nil }
.assign(to: &$showAlert)
}
}
struct AlertOverlay<Content: View>: View {
var content: Content
#ObservedObject var alertModel: AlertModel
init(
alertModel: AlertModel,
#ViewBuilder content: #escaping () -> Content
) {
self.alertModel = alertModel
self.content = content()
}
var body: some View {
ZStack {
content
.blur(radius: alertModel.showAlert
? UserInterfaceStandards.blurRadius
: 0)
}
.alert(isPresented: $alertModel.showAlert) {
guard let alertState = alertModel.alertState else {
return Alert(title: Text("Unexected internal error as occured."))
}
return Alert(title: Text(alertState.title))
}
}
}

SwiftUI alternate views - Protocol type 'Any' cannot conform to 'View' because only concrete types can conform to protocols

I have two reusable views, Ex1 and Ex2. I am trying to show one of them depends on a condition alternately but I could not it.
ContentvIew:
struct ContentView: View {
#State var selector = false
var cvc = ContentViewController()
var body: some View {
ZStack { // ERROR: Protocol type 'Any' cannot conform to 'View' because only concrete types can conform to protocols
cvc.getView(t: selector)
Button(action: {
self.selector.toggle()
print(self.selector)
}) {
Text("Button")
}
}
}
}
Ex1 :
import SwiftUI
struct Ex1: View {
var body: some View {
Text("Ex 1")
}
}
Ex2 :
import SwiftUI
struct Ex2: View {
var body: some View {
Text("Ex 2")
}
}
ContentViewController :
import Foundation
class ContentViewController {
let a = Ex1()
let b = Ex2()
func getView (t: Bool) ->(Any){
if t {
return a
}
else {
return b
}
}
}
I think it is very simple but not for me, for now. Help for two things, please.
I want to understand this problem, and the solution.
Best way for alternate two view in a layout.
Thanks in advance.
As the error suggests the return type specified in ContentViewController's getView method does not conform to the protocols.
In SwiftUI everything you specified in body{} clause must be a type of View if you do not know what kind of view available at runtime.
You can specify AnyView type for unknown views.
So your error will be removed by changing the ContentViewController's code.
class ContentViewController {
let a = Ex1()
let b = Ex2()
func getView (t: Bool) -> (AnyView) {
if t {
return AnyView(a)
}
else {
return AnyView(b)
}
}
}