Satisfying var body: some View in protocol extension - swift

I wish to do the following:
import SwiftUI
protocol CombinedView: View {
var dataForViewA: String { get }
var viewA: any View { get }
var viewB: any View { get }
}
extension CombinedView {
var viewA: Text {
Text(dataForViewA)
}
var body: some View {
VStack {
viewA
viewB
}
}
}
viewA works fine because I can specify the concrete type, but var body complains:
Type 'any View' cannot conform to 'View'
I am unsure what I need to implement to solve this. Any ideas?
Thanks in advance for any advice

It does not work that way, instead we need associated types for each generic view and specialisation in extension.
Here is fixed variant. Tested with Xcode 14b3 / iOS 16
protocol CombinedView: View {
associatedtype AView: View
associatedtype BView: View
var dataForViewA: String { get }
var viewA: Self.AView { get }
var viewB: Self.BView { get }
}
extension CombinedView {
var body: some View {
VStack {
viewA
viewB
}
}
}
extension CombinedView where AView == Text {
var viewA: Text {
Text(dataForViewA)
}
}
Test module on GitHub

Related

SwiftUI factory method issue in subclasses

I develop a SwiftUI app in which I created a model entities inheritance hierarchy. The entities have a view property that returns a View instance. The intention is to build a List from an array of entities.
The model code:
protocol EntityProtocol: Identifiable {
associatedtype Content: View
#ViewBuilder var view: Content { get }
}
extension EntityProtocol {
#ViewBuilder var view: some View {
EmptyView()
}
}
class RootEntity: EntityProtocol {
let id = UUID()
}
class EntityA: RootEntity {
}
extension EntityA {
#ViewBuilder var view: some View {
Text("This is from EntityA")
}
}
The intention is when I have multiple subclasses of RootEntity each of the subclasses will return its own view.
Then I have my ContentView:
struct ContentView: View {
var entities: [RootEntity] = [EntityA()]
var body: some View {
List {
ForEach(entities) { entity in
entity.view
}
}
}
}
I have one item in the entities array but I get an empty view as a result, the view property is not called inside ForEach.
However, when I type cast to EntityAn inside ForEach it works as expected and shows "This is from EntityA" label:
struct ContentView: View {
var entities: [RootEntity] = [EntityA()]
var body: some View {
List {
ForEach(entities) { entity in
if let entityA = entity as? EntityA {
entityA.view
}
}
}
}
}
I'm using Xcode 14.2
Your suggestions on what's going on are welcome. And please tell me if I'm doing something your not supposed to be doing with SwiftUI.
Thanks!

Using protocol to capture SwiftUI view

I’m trying to write a protocol that requires conformers to have a View property, or a #ViewBuilder method that returns some View.
I want to have a re-useable composite view that can build different sub views based on what type of data needs to be displayed.
The protocol would look like this…
protocol RowView {
var leftSide: some View { get }
var rightSide: some View { get }
}
That way I could call something like this…
struct Example: RowView {
var id: Int
var leftSide: some View { … }
var rightSide: some View { … }
}
struct ContentView: View {
let rows: [RowView]
var body: some View {
VStack {
Foreach(rows, id: \.id) {
HStack {
$0.leftSide
$0.rightSide
}
}
}
}
}
You need to change the protocol to something like:
protocol RowView {
associatedtype LView: View
associatedtype RView: View
var leftSide: LView { get }
var rightSide: RView { get }
}
Also, use the concrete Example type in the content view instead of the protocol (the protocol you defined doesn't have id at all):
let rows: [Example]
Also! you can make the RowView to be identifiable as your need, So no need for id: \.id anymore:
protocol RowView: Identifiable
A working code:
protocol RowView: Identifiable {
associatedtype LView: View
associatedtype RView: View
var leftSide: LView { get }
var rightSide: RView { get }
}
struct Example: RowView {
var id: Int
var leftSide: some View { Text("Left") }
var rightSide: some View { Text("Right") }
}
struct ContentView: View {
let rows: [Example] = [
.init(id: 1),
.init(id: 2)
]
var body: some View {
VStack {
ForEach(rows) { row in
HStack {
row.leftSide
row.rightSide
}
}
}
}
}

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()
}
}
}

Reference to generic type when returning view that uses a protocol

I am being unable to return a View that uses a protocol as a dependency as this is throwing me Reference to generic type 'LoginView' requires arguments in <...>.
func makeLoginView(viewModel: LoginViewModelType) -> LoginView {
return LoginView(viewModel: viewModel)
}
My LoginView uses LoginViewModelType as I have two different view models.
protocol LoginViewModelType: ObservableObject {
var bookingPaymentViewModel: BookingPaymentViewModel? { get }
var email: String { get set }
var password: String { get set }\
...
func login()
}
struct LoginView<ViewModel: LoginViewModelType>: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
...
}
I can't understand what I am doing wrong, the LoginView should be able to return a View regardless as it complies to LoginViewModelType.
You need to constrain makeLoginView to accept a generic of type LoginViewModelType and then use that same generic in the returned value.
class BookingPaymentViewModel { }
func makeLoginView<T:LoginViewModelType>(viewModel: T) -> LoginView<T> {
return LoginView(viewModel: viewModel)
}
protocol LoginViewModelType: ObservableObject {
var bookingPaymentViewModel: BookingPaymentViewModel? { get }
var email: String { get set }
var password: String { get set }
func login()
}
struct LoginView<ViewModel: LoginViewModelType>: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
Text("test")
}
}

SwiftUI can not return View on function

I'm trying return View based selected menu item on function. But it throws error:
Protocol 'View' can only be used as a generic constraint because it has Self or associated type requirements.
There my code:
enum MenuItem {
case Main
case Report
}
struct Menu: View {
#State var activeItem: MenuItem = .Main
private func getActiveView() -> View {
switch activeItem {
case .Main:
return DashboardView()
case .Report:
return ReportView()
}
}
var body: some View {
...
getActiveView()
...
}
}
struct DashboardView: View {
var body: some View {
Text("Contact")
}
}
struct ReportView: View {
var body: some View {
Text("Contact")
}
}
Im new on SwiftUI. Any ideas how to return View?
SwiftUI 2
Here is a solution tested with Xcode 12b / iOS 14
struct Menu: View {
#State var activeItem: MenuItem = .Main
// make function ViewBuilder
#ViewBuilder
private func getActiveView() -> some View {
switch activeItem {
case .Main:
DashboardView() // don't use 'return' as it disables ViewBuilder
case .Report:
ReportView()
}
}
var body: some View {
getActiveView()
}
}
SwiftUI gives us a type-erased wrapper called AnyView that we can return.
Tested Solution:
struct Menu: View {
#State var activeItem: MenuItem = .Main
func getActiveView() -> some View {
switch activeItem {
case .Main:
return AnyView(DashboardView())
case .Report:
return AnyView(ReportView())
}
}
var body: some View {
getActiveView()
}
}
Note: type-erased wrapper effectively forces Swift to forget about what specific
type is inside the AnyView, allowing them to look like they are the
same thing. This has a performance cost, though, so don’t use it
often.
For more information you can refer to the this cool article: https://www.hackingwithswift.com/quick-start/swiftui/how-to-return-different-view-types