I have subclasses of an Object class, that I want to provide content views. The concrete example is a Panel subclass that has an array of Objects (eg TextObject, ImageObject, etc). The aim is to use a:
ForEach(objects, id:.id) { object in
object.contentView()
}
I cannot provide a:
func contentView() → some View
in Object since this cannot be overridden in subclasses (some View needs a generic constraint etc error)
I can do this with:
func contentView() → AnyView
but I’m told I shouldn’t for the reasons re-iterated in your article.
I’ve tried protocols and looked at StackOverflow for a solution (including a post by Jessy Caterwaul) but none of them address returning a View.
I cannot get it to work in the format of something like:
func contentView<T: View>() → View
I would like to utilize a Protocol to do this, so each object can conform, but then my problem is to load these in from a file, where I need to use a string to work out what subclass of Object each is. I can do this using:
if let className = dict[Keys.className.rawValue] as? String,
let theClass = Bundle.main.classNamed(className) as? Object.Type {
return theClass.init(from: dict)
}
But then I think I’m stuck using Object and subclasses.
Is there an official way of creating objects, saving their properties in a file (currently dictionaries of [String: Any]), then being able to use a protocol to show content in SwiftUI View?
There are many official ways, Apple has allowed you to do Object to Storage to Object for a long time. Here are some options:
And if you prefer to store in a database, that is exactly what Core Data is for.
Thanks Roberto for your initial comments. Do you have any thoughts about the first part of my question? In particular, how to avoid AnyView in subclasses? I would prefer to avoid subclassing completely but cannot work out how to do this using Protocols. I’ve read Kodeco’s tutorials on protocols, opaque types and type erasure but can’t get past compiler errors.
I’m not sure what you are trying to accomplish, but sounds a bit complex - that typically is a smell that you should rethink your approach.
If you are trying to compose some complex views that are passed along to others, consider ViewBuilder.
Here’s a concrete example which I hope helps.
Aim: to provide an array of TestObjects that can respond in a View body to calls:
array.displayView(), and separately array.editView() to present each TestObject’s differing options.
protocol ViewsProtocol: Identifiable, Equatable {
var id: UUID { get set }
func displayView() → AnyView
func editView() → AnyView
}
// This is what I tried to do using “some View” but it does not compile with error:
// ‘some’ type cannot be the return type of a protocol requirement; did you mean to add an associated type?
protocol ViewsProtocol: Identifiable, Equatable {
var id: UUID { get set }
func displayView() → some View
func editView() → some View
}
// This compiles but is it better than my AnyView version or just the same for the SwiftUI compiler, in that it cannot do optimizations because it does not know what View to expect?
protocol ViewsProtocol2: Identifiable, Equatable {
var id: UUID { get set }
func displayView() → any View
func editView() → any View
}
struct TestView: ViewsProtocol2 {
var id: UUID
func displayView() -> any View {
return EmptyView()
}
func editView() -> any View {
return Text("Blah")
}
}
// I separated the two protocols with some success:
protocol DisplayProtocol: Identifiable, Equatable {
associatedtype T: View
func displayView() → T
}
protocol EditProtocol: Identifiable, Equatable {
associatedtype T: View
func editView() → T
}
struct TestObject1: DisplayProtocol {
var id: UUID = UUID()
func displayView() -> some View {
EmptyView()
}
}
struct TestObject2: EditProtocol {
var id: UUID = UUID()
func editView() -> some View {
Text("Blah")
}
}
// But this next complains this object does not satisfy the EditProtocol, and I cannot fix it.
struct TestObject3: DisplayProtocol, EditProtocol {
var id: UUID = UUID()
func displayView() -> some View {
EmptyView()
}
func editView() -> some View {
Text("Blah")
}
}
// I need the objects to respond to both methods since they will be stored in an array of different types of TestObjects
So what am I missing to fix this please?
Could you please format your code? This will help those who are seeking to provide advice to you:
// use ```swift code ```
func displayView() -> some View {
EmptyView()
}
func editView() -> some View {
Text("Blah")
}
Sorry about the formatting, this should be better.
The code below works…
protocol ViewsProtocol: Identifiable, Equatable {
var id: UUID { get set }
func displayView() -> AnyView
func editView() -> AnyView
}
… but uses AnyView, so I tried other variations (with some success at the bottom)
protocol ViewsProtocol: Identifiable, Equatable {
var id: UUID { get set }
func displayView() -> some View
func editView() -> some View
}
This uses “some View” but it does not compile with error:
‘some’ type cannot be the return type of a protocol requirement; did you mean to add an associated type?
So I tried:
protocol ViewsProtocol2: Identifiable, Equatable {
var id: UUID { get set }
func displayView() -> View
func editView() -> View
}
but the compiler told me I had to use “any View” (presumably to type erase), so I tried:
protocol ViewsProtocol2: Identifiable, Equatable {
var id: UUID { get set }
func displayView() -> any View
func editView() -> any View
}
class TestView: ViewsProtocol2 {
var id: UUID = UUID()
static func ==(lhs: TestView, rhs: TestView) -> Bool {
return lhs.id == rhs.id
}
func displayView() -> any View {
return EmptyView()
}
func editView() -> any View {
return Text("Blah")
}
}
class TestViewSubclass: TestView {
override func displayView() -> any View {
return Image("smiley")
}
override func editView() -> any View {
return Text("Some text")
}
}
This would not compile using TestView as a struct, but does as a class as above. But it uses “any View” and is it better than my AnyView version or just the same for the SwiftUI compiler, in that it cannot do optimizations because it does not know what View to expect?
I then tried to separate the two protocols with some success?
protocol DisplayProtocol: Identifiable, Equatable {
associatedtype T: View
func displayView() -> T
}
protocol EditProtocol: Identifiable, Equatable {
associatedtype U: View
func editView() -> U
}
class BaseObject: DisplayProtocol, EditProtocol {
var id: UUID = UUID()
static func ==(lhs: BaseObject, rhs: BaseObject) -> Bool {
return lhs.id == rhs.id
}
func displayView() -> some View {
Image("Shadow")
}
func editView() -> some View {
Text("Blah2")
}
}
class BaseObjectSubclass: BaseObject {
func displayView() -> some View {
Image("Shadow")
}
func editView() -> some View {
Text("Blah2")
}
}
class PanelClass: Identifiable {
var id: UUID = UUID()
var objects: [BaseObject] = []
}
struct PanelClassView: View {
@State var panelClass: PanelClass
var body: some View {
ForEach(panelClass.objects, id: \.id) { object in
object.displayView()
object.editView()
}
}
}
This does compile (after I changed the EditProtocol to “U” rather than “T” - not sure why this caused a conflict).
So with experimentation, I think I found a solution! So can you see why this should not work?
And I just realized that a problem with this code, is that when I call the PanelClassView ForEach for each objects array object, they will use the BaseObject displayView and editView functions, not the overridden ones (since the compiler does not recognise base class “some View” functions as the same.
I am trying to use a type erasure for an object that uses both protocols but have not found a solution using “some View”. I can do it if I use AnyView (eg as below, if I declare id: UUID { get set } and put both display and edit view funcs in the same ViewsProtocol):
public struct AnyViewsProtocol: ViewsProtocol {
public static func == (lhs: AnyViewsProtocol, rhs: AnyViewsProtocol) -> Bool {
lhs.id == rhs.id
}
public var id: UUID
private let wrappedDisplayView: () -> AnyView
private let wrappedEditView: () -> AnyView
init<T: ViewsProtocol>(_ views: T) {
id = views.id
wrappedDisplayView = views.displayView
wrappedEditView = views.editView
}
public func displayView() -> AnyView {
return wrappedDisplayView()
}
public func editView() -> AnyView {
return wrappedEditView()
}
}
I can then call the PanelClassView ForEach on the objects if they are wrapped in an AnyViewsProtocol(object) to call their own display and edit funds.
But I still cannot work out how to do this with the separate DisplayProtocol and EditProtocol version.
I’m sorry, but I still don’t understand what you are trying to accomplish. And yes, when doing some View or AnyView you have type erased which means that any connection back to the original object is gone. I see you trying to accomplish something with code, but I don’t understand the purpose, perhaps starting there would be a first step? Some other approach my work.
Aim:
To create a customizable cognitive test based on different “controls”. These controls (or objects) need to be loaded in from a file and recognized by their class name, then loaded into panels (which have arrays of these objects) to be displayed on iOS.
Background:
I have an existing working version in Objective-C (created 10 years ago). I an updating this to use SwiftUI (which in theory should be much simpler and flexible).
The current Obj-C version has test files in xml that are loaded from disc. I would prefer not to have to re-write all these files again, so want to be able to load them into my SwiftUI version.
The panel does not need to know which specific object is has loaded, but does need to be able to call both the displayView() and editView() funcs at appropriate times.
The displayView() will show the object’s controls in the panel’s View for testing.
The editView() will show the object’s editable version (to change settings) in another panel View for editing.
I have sorted out how to load and init using the class name only (eg):
static func create(from dict: [String : Any]) -> Object? {
// get the class string
if let className = dict[Keys.className.rawValue] as? String,
let theClass = Bundle.main.classNamed(className) as? Object.Type {
return theClass.init(from: dict)
}
return nil
}
And saving back to the file uses each object’s dictionary.
I can accomplish the display and editing using a protocol with results of AnyView (eg):
protocol ViewsProtocol: Identifiable, Equatable {
var id: UUID { get set }
func displayView() -> AnyView
func editView() -> AnyView
func dictionary() -> [String: Any]
func translationStrings() -> [String]
}
// wrapper for ViewsProtocol
public struct AnyViewsProtocol: ViewsProtocol {
public static func == (lhs: AnyViewsProtocol, rhs: AnyViewsProtocol) -> Bool {
lhs.id == rhs.id
}
public var id: UUID
private let wrappedDisplayView: () -> AnyView
private let wrappedEditView: () -> AnyView
private let wrappedDictionary: () -> [String: Any]
private let wrappedTranslationStrings: () -> [String]
init<T: ViewsProtocol>(_ views: T) {
id = views.id
wrappedDisplayView = views.displayView
wrappedEditView = views.editView
wrappedDictionary = views.dictionary
wrappedTranslationStrings = views.translationStrings
}
public func displayView() -> AnyView {
return wrappedDisplayView()
}
public func editView() -> AnyView {
return wrappedEditView()
}
public func dictionary() -> [String : Any] {
return wrappedDictionary()
}
public func translationStrings() -> [String] {
return wrappedTranslationStrings()
}
}
extension ViewsProtocol {
func asAnyViewsProtocol() -> AnyViewsProtocol {
return AnyViewsProtocol(self)
}
}
But my question is how to get rid of the AnyView?
The rest of the examples I have given above show that I can not been able to remove AnyView or any View to solve the panel calling of displayView and editView, as in (for display):
ForEach(panel.objects, id: \.id) { object in
object.displayView()
.padding(.horizontal, 20)
.padding(.vertical, 5)
}
Is this clearer? Is there a solution you can point me to, that eliminates the use of AnyView?
Thanks for any assistance.
Where is that ForEach contained? Is it a List or Stack? IF not, please consider as mentioned above a ViewBuilder.
Also, take a look at this article: Apple Developer Documentation
Dear Roberto
Thank you for your responses, but why would it matter whether it is in a List or a Stack?
The issue I am having trouble with is trying to avoid using AnyView because all the tutorials and Apple documentation I’ve read advises to avoid AnyView so SwiftUI can optimise the View components. Do you have source code or experience avoiding AnyView for this situation?
So yes, AnyView does destroy all components on updates, so if you have a giant hierarchy, it would take more time to re-build on an update.
To answer you question: Stacks, Sections, etc can be used to compose multiple views. Group - a fast shiny way to not have to use AnyView to compose multiple views: Apple Developer Documentation
A few things:
- Your code seems to be using SwiftUI like UIKit, where the code and the views are together. The whole point of SwiftUI is to separate that.
- How big are your hierarchies? Have your run a profiler to test out the performance hit? If you haven’t yet, read this: Performance Battle: AnyView vs Group - Alexey Naumov
Dear Roberto
With respect, you are not addressing my question. Have you read the last few responses? Your own responses are so generic they are unhelpful.
I have given examples of the problem, which is that the compiler accepts AnyView as a func result but not “some View” and I need to be able to call displayView() and editView() on different types of objects which return different types of Views. @ViewBuilder does not solve this problem. Use of generic types Group, VStack, HStack, ZStack etc does not compile as protocol func results either.
If no-one else is kind enough to tackle this, then so be it, I will have to seek help elsewhere.
Sorry your question hasn’t been answered. As I mentioned above, I’m not sure I understand what you are trying to accomplish. Sounds like AnyView does what you want, but you are concerned with performance? I was hopeful the AnyView vs Group article would help. At the very least, please try profiling.
This topic was automatically closed after 166 days. New replies are no longer allowed.