I have a PersonView like following
struct PersonView<Content>: View where Content: View {
let text: Text
let detailText: Text
var content: (() -> Content)? = nil
var body: some View {
VStack(alignment: .leading, spacing: 10) {
text
detailText
content?()
}
}
}
Also My sample model looks like:
struct Person {
let name: String
let age: Int
}
To make life easy I have made an extension of PersonView
extension PersonView {
init<Content: View>(person: Person, content: (() -> Content)? = nil) {
self.init(text: Text(person.name),
detailText: Text("\(person.age)"), content: content)
}
}
but here I am getting error like
"Cannot convert value of type '(() -> Content)?' to expected argument type 'Optional<(() -> _)>'"
I am not sure where I am getting wrong
You should not declare an extra generic parameter Content in the initialiser. The initialiser should not be generic, and instead just use the Content generic parameter from PersonView:
extension PersonView {
init(person: Person, content: (() -> Content)? = nil) {
self.init(text: Text(person.name),
detailText: Text("\(person.age)"), content: content)
}
}
The extra Content that you declared is a different generic parameter from the Content in PersonView<Content>, which is why the compiler says it can't convert the types.
Related
update: add same error about Hashable
I have created an Identifiable compliant protocol and compliant structures. Then, when I create the list and reference it in ForEach, I get the error Type 'any TestProtocol' cannot conform to 'Identifiable'(I get the same error about Hashable).
How should I fix this program?
If I write ForEach(list, id: \.id) , it works, but I don't think it makes sense to be Identifiable compliant.
import SwiftUI
protocol TestProtocol: Identifiable, Hashable {
var id: UUID { get set }
var name: String { get set }
func greeting() -> String
static func == (lhs: Self, rhs: Self) -> Bool
}
extension TestProtocol {
static func == (lhs: Self, rhs: Self) -> Bool {
return lhs.id == rhs.id
}
}
struct Person: TestProtocol {
var id = UUID()
var name: String
func greeting() -> String {
return "my name is \(name) and I'm a human."
}
}
struct Dog: TestProtocol {
var id = UUID()
var name: String
func greeting() -> String {
return "my name is \(name) and I'm a dog."
}
}
struct ContentView: View {
var list: [any TestProtocol] = [Person(name: "p1"), Dog(name: "d1")]
#State var selected: any TestProtocol
var body: some View {
VStack {
Picker(selection: $selected) { // Type 'any TestProtocol' cannot conform to 'Hashable'
ForEach(list) { l in // Type 'any TestProtocol' cannot conform to 'Identifiable'
Text(l.greeting()).tag(l) // Type 'any TestProtocol' cannot conform to 'Hashable'
}
} label: {
Text("select")
}
}
}
}
Your error message complaining about Hashable is a "red hering". The protocol TestProtocol, and therefor all structs conforming to it, conforms to Hashable.
let person = Person(name: "IAmHashable")
print(person.hashValue)
The reason this is failing is within the Picker. It needs a concrete type and not a protocol. One solution would be to create a "Container" type and a custom binding that handles this.
struct Container: Identifiable, Hashable{
//implement the same equality as in your TestProtocol
static func == (lhs: Container, rhs: Container) -> Bool {
rhs.wrapped.id == lhs.wrapped.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(wrapped)
}
var wrapped: any TestProtocol
//for convenience
var id: UUID {wrapped.id}
}
and the ContentView:
struct ContentView: View {
let startArr: [any TestProtocol] = [Person(name: "p1"), Dog(name: "d1")]
#State private var selected: (any TestProtocol)?
var body: some View {
// list of wrapped protocols
var list: [Container] = { startArr.map{Container(wrapped: $0)}}()
// binding
let selectionBinding: Binding<Container> = .init {
let returninstance = list.first { cont in
cont.id == selected?.id
}
return returninstance ?? list[0]
} set: { container in
selected = container.wrapped
}
// viewCode
VStack {
Picker(selection: selectionBinding) {
ForEach(list) { l in
Text(l.wrapped.greeting())
.tag(l)
}
} label: {
Text("select")
}
// confirmation selection changed
Text(selected?.name ?? "no Selection")
}
}
}
Remarks:
This solution has a few drawbacks:
your initial array startArr should never be empty, else return returninstance ?? list[0] will break you code. This can be handled, but I think this is out of the scope of this question.
the equality comparison of Container needs to be the same as in TestProtocol as you cannot compare two any Protocols
on the appearance of ContainerView selected will be nil until something is selected. Any usage, e.g.: the Text element in this case, needs to deal with this. But you could probably set this in .onApear.
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()
}
}
}
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()
}
}
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?
I have a generic section view component, that should be initialised to return some view for edit of the section and another one for adding elements to the section.
import SwiftUI
enum ProfileSectionType {
case editable
case addable
}
struct ProfileSection<Content> : View where Content : View {
var model:ProfileSectionModel? = nil
var sectionType:ProfileSectionType = .editable
var onClick:(() -> View)? = nil
var content: Content
#inlinable public init(_ model:ProfileSectionModel? = nil, sectionType:ProfileSectionType = .editable, #ViewBuilder content: () -> Content) {
self.model = model
self.content = content()
self.sectionType = sectionType
}
var body : some View {
switch sectionType {
case .editable:
return AnyView(editable())
case .addable:
return AnyView(addable())
}
}
}
But it is not possible to use a closure like (() -> View). I could have passed it as an parameter to the init, but then I would not get lazy loading of the view.
I have also tried to use generic in the enum. My first attempt were something like:
enum ProfileSectionType<T : View> {
case editable
case addable(viewForAdding:T)
}
But that seemed to make it more complex. How can get this section to create two different views for edit and add?
I want to use it like this:
ProfileSection(m.identifications(), sectionType: .addable, onClick: ^{
AddIdentificationView()
}){
ForEach( 0 ..< m.identifiers().count ) {
ProfileEditableItem(key: m.identifiers()[$0].title, value: m.identifiers()[$0].value)
}
}
I have solved it by setting:
struct ProfileSection<Content, T> : View where Content : View, T : View {
var model:ProfileSectionModel? = nil
var sectionType:ProfileSectionType = .editable
var onClickAdd:(() -> T)? = nil
var content: Content
I can then use it like this:
ProfileSection<ForEach, AddIdentificationView>(m.identifications(), sectionType: .addable, editDialog: {
AddIdentificationView()
}){
ForEach( 0 ..< m.identifiers().count ) {
ProfileEditableItem(key: m.identifiers()[$0].title, value: m.identifiers()[$0].value)
}
}
The AddIdentificationView is then presented in a modal view. It would be cleaner if I got rid of <ForEach, but it will do for now.