Using protocol in SwiftUI for providing "some View" / Generics? - swift

I'm trying to get my head something in SwiftUI. I want to build a SwiftUI view and have something you could call a ViewProvider as a #State var. something like this:
protocol ViewProvider {
associatedtype ViewOne = View
associatedtype ViewTwo = View
#ViewBuilder var viewOne: ViewOne { get }
#ViewBuilder var viewTwo: ViewTwo { get }
}
struct ContentView: View {
#State private var parent: ViewProvider?
var body: some View {
VStack {
HStack {
Button(action: { parent = Father() }, label: { Text("Father") })
Button(action: { parent = Mother() }, label: { Text("Mother") })
}
if let p = parent {
p.viewOne
p.viewTwo
}
}
}
}
class Father: ViewProvider {
#ViewBuilder var viewOne: some View {
Text("Father One!")
}
#ViewBuilder var viewTwo: some View {
Text("Father Two!")
}
}
class Mother: ViewProvider {
#ViewBuilder var viewOne: some View {
Text("Mother One!")
}
#ViewBuilder var viewTwo: some View {
Text("Mother Two!")
}
}
This produces 2 different compiler errors.
#State private var parent: ViewProvider?
// Protocol 'ViewProvider' can only be used as a generic constraint because it has Self or associated type requirements
and
p.viewOne
p.viewTwo
// 2x Member 'viewOne' cannot be used on value of protocol type 'ViewProvider'; use a generic constraint instead
I have a vague idea of what I'm doing wrong, but no idea on how to solve it :)
What syntax should I use to get something like this to work?

Assuming you're on Swift 5.6 or lower, the problem is that you can only use protocols with associated types for conformance, ie you can't use them as types to pass around. The reasoning is that their associated types will be different for different conformers.
Say you have the following:
protocol P {
associatedtype T
var prop: T
}
class MyClass: P {
var prop: Int
}
class MyOtherClass: P {
var prop: String
}
What would the result of the following be?
let arr: [P] = [MyClass(), MyOtherClass()]
let myMappedArr = arr.map { $0.prop }
prop is of a different type for each conformer.
In Swift 5.7, however, you actually can pass around protocols of this sort. In later versions of Swift, you will have to use the keyword any to pass these protocols around as types.
See the proposal for unlocked existentials to learn more about it.
Lastly to address opaque types here:
Since you can't pass around protocols with associated types, you can't have something like
#State var myState: ViewProvider or even #State var myState: some ViewProvider, because your state variable is assigned, and you can't assign something of an opaque type.
In SwiftUI's View, this works because the view property is computed, and thus the type can be inferred
// type is inferred to be (something like) Group<Text>
var body: some View {
Group {
Text("something")
}
}
whereas here, you can't find a suitable type to assign to a property whose type is opaque
#State var myState: some ViewProvider
...
// You don't know myState's type, so you can't assign anything to it
myState = ... // error - you won't be able to find a matching type to assign to this property
To wit, the line #State private var parent: ViewProvider? in your code simply won't compile in Swift 5.6 or lower, because you're not allowed to use your ViewProvider protocol as a type for anything other than conformance or as an opaque return type when used in functions or computed properties.
Sorry for all the edits. Wanted to provide a couple of potential solutions:
One way is to simply make your ContentView generic over the type of its ViewProvider
struct ContentView<ViewProviderType: ViewProvider> {
#State private var parent: ViewProviderType?
...
}
The other would be to simply remove the associatedtype from your protocol and just erase the view type you're trying to return:
protocol ViewProvider {
var viewOne: AnyView { get }
var viewTwo: AnyView { get }
}
If you're working with Swift 5.7, you may be able to use your type-constrained protocols as property types, or you can also use primary associated types, wherein you could declare properties of type ViewProvider<MyView> (though that doesn't necessarily solve your problem).
Generics or type erasure over ViewProvider's view types are probably the best candidates for what you're trying to do, even in a Swift 5.7 world.

Related

Binding to subscript doesn't update TextField (macOS)

I have this Store struct, which is a wrapper for all my data. It has a subscript operator which takes in a UUID, and returns the associated object.
This way, I can have a List bind to a selection variable, which has type UUID, and then in another view I can access the selected object from that UUID.
However, I'm experiencing an issue where my TextField which binds to the Store doesn't update. It does update if I wrap it in another Binding, or if I instead just use Text.
Here is an minimal reproducible example:
struct Person: Identifiable, Hashable {
let id = UUID()
var name: String
}
struct Store {
var data: [Person]
subscript(id: Person.ID) -> Person {
get {
data.first(where: { $0.id == id })!
}
set {
data[data.firstIndex(where: { $0.id == id })!] = newValue
}
}
}
struct ContentView: View {
#State var store = Store(data: [
Person(name: "Joe"),
Person(name: "Eva"),
Person(name: "Sam"),
Person(name: "Mary")
])
#State var selection: Person.ID?
var body: some View {
NavigationView {
List(store.data, selection: $selection) {
Text($0.name)
}
if let selection = selection {
// Creating a new Binding which simply wraps $store[selection].name
// fixes this issue. Or just using Text also works.
TextField("Placeholder", text: $store[selection].name)
}
else {
Text("No Selection")
}
}
}
}
To reproduce this issue, just click different names on the Sidebar. For some reason the detail view's TextField doesn't update!
This issue can also be resolved if we simply move the Store to a ObservableObject class with #Published.
Also, making the Store conform to Hashable doesn't help this issue.
I feel like I'm missing something very basic with SwiftUI. Is there any way to fix this?
EDIT:
I've changed out Store for an [Person], and I made an extension with the same subscript operator that is in Store. However, the problem still remains!
try this:
TextField("Placeholder", text: $store[selection].name)
.id(selection) // <-- here

Swift recursive protocol: How to mark leave node?

I've a tree of (different) structs which I want to show in an NSOutlineView. I've also written an update function which determines the move/insert/reload operations on the outline view.
To make the update function more generic, I've written a protocol that makes the different structs similar:
protocol Outlinable: Equatable, Identifiable {
associatedtype T: Outlinable
var children: [T]? { get }
}
The children array is an optional to mark that a node might not have children.
I've made two structs conform to this protocol:
struct ImageWithErrors: Decodable, FetchableRecord {
let imageFile: ImageFile
let imageFileErrors: [ImageFileError]
}
struct LayerImagesListItem: Decodable, FetchableRecord {
let layer: Layer
let imageFiles: [ImageWithErrors]
}
extension LayerImagesListItem: Identifiable, Outlinable {
var id: Int64 { get { layer.id! }}
var children: [ImageWithErrors]? { get { imageFiles }}
}
extension ImageWithErrors: Identifiable, Outlinable {
var id: Int64 { get { -imageFile.id! }}
var children: [Outlinable]? { get { return nil }}
}
The LayerImagesListItem is a root struct, while the ImageWithErrors is (currently) a leave struct. But on this last struct I get the errors:
Type 'ImageWithErrors' does not conform to protocol 'Outlinable'
Protocol 'Outlinable' can only be used as a generic constraint because it has Self or associated type requirements
I've tried replacing [Outlinable] with [Any] but that doesn't solve anything.
How can I tell Swift that ImageWithErrors is never going to return any children?

SwiftUI ForEach Binding compile time error looks like not for-each

I'm starting with SwiftUI and following WWDC videos I'm starting with #State and #Binding between two views. I got a display right, but don't get how to make back-forth read-write what was not include in WWDC videos.
I have model classes:
class Manufacturer {
let name: String
var models: [Model] = []
init(name: String, models: [Model]) {
self.name = name
self.models = models
}
}
class Model: Identifiable {
var name: String = ""
init(name: String) {
self.name = name
}
}
Then I have a drawing code to display that work as expected:
var body: some View {
VStack {
ForEach(manufacturer.models) { model in
Text(model.name).padding()
}
}.padding()
}
and I see this:
Canvas preview picture
But now I want to modify my code to allows editing this models displayed and save it to my model #Binding so I've change view to:
var body: some View {
VStack {
ForEach(self.$manufacturer.models) { item in
Text(item.name)
}
}.padding()
}
But getting and error in ForEach line:
Generic parameter 'ID' could not be inferred
What ID parameter? I'm clueless here... I thought Identifiable acting as identifier here.
My question is then:
I have one view (ContentView) that "holds" my datasource as #State variable. Then I'm passing this as #Binding to my ManufacturerView want to edit this in List with ForEach fill but cannot get for each binding working - how can I do that?
First, I'm assuming you have something like:
#ObservedObject var manufacturer: Manufacturer
otherwise you wouldn't have self.$manufacturer to begin with (which also requires Manufacturer to conform to ObservableObject).
self.$manufacturer.models is a type of Binding<[Model]>, and as such it's not a RandomAccessCollection, like self.manufacturer.models, which is one of the overloads that ForEach.init accepts.
And if you use ForEach(self.manufacturer.models) { item in ... }, then item isn't going to be a binding, which is what you'd need for, say, a TextField.
A way around that is to iterate over indices, and then bind to $manufacturer.models[index].name:
ForEach(manufacturer.indices) { index in
TextField("model name", self.$manufacturer.models[index].name)
}
In addition to that, I'd suggest you make Model (and possibly even Manufacturer) a value-type, since it appears to be just a storage of data:
struct Model: Identifiable {
var id: UUID = .init()
var name: String = ""
}
This isn't going to help with this problem, but it will eliminate possible issues with values not updating, since SwiftUI wouldn't detect a change.

SwiftUI - PageView - Pass in different Views

I successfully implemented PageView within SwiftUI via thread:
How to implement PageView in SwiftUI?
Passing in multiple Views via an Array works like a charm, as long as all views are of the same struct.
PageView([TestView(), TestView()]).
However, I'd like to pass in different views.
PageView([TestView(), AnotherView(), DifferentView()]).
All views are of SwiftUI type:
struct NAME : View { code }
When I try to add different structs to an array I get the following error message:
var pageViewViewArray = [TestView(), AnotherView(), DifferentView()]
Heterogeneous collection literal could only be inferred to '[Any]';
add explicit type annotation if this is intentional.
Insert' as [Any]
By casting it to:
var pageViewViewArray = [TestView(), AnotherView(), DifferentView()] as! [Any]
PageView(pageViewViewArray)
PageView will say:
Protocol type 'Any' cannot conform to 'View' because only concrete types can conform to protocols
I'll greatly appreciate any ideas.
Try using type erasure by casting every view to AnyView:
var pageViewViewArray: [AnyView] = [AnyView(TestView()), AnyView(AnotherView()), AnyView(DifferentView())]
Documentation here, and an example of using it here.
There is a more efficient way to do it, without type erasure. You should create a common view in which you inject an enum value based on which you then return a desired view. Take a look at an example below:
/// The first type of a view that accepts and shows a number
struct ViewA: View {
let number: Int
var body: some View {
Text("\(number)")
}
}
/// The second type of a view that accepts and shows a string
struct ViewB: View {
let string: String
var body: some View {
Text(string)
}
}
/// An enum type used for dependency injection
enum ViewType {
case viewA(number: Int)
case viewB(string: String)
}
/// A common view which internally handles different kind of views based on injected enum value
struct CommonView: View {
let viewType: ViewType
var body: some View {
switch viewType {
case .viewA(let number):
ViewA(number: number)
case .viewB(let string):
ViewB(string: string)
}
}
}
// Views used for page view controller, that are now of same type:
var pageViewViewArray = [
CommonView(viewType: .viewA(number: 1)),
CommonView(viewType: .viewB(string: "Hello, World!"))
]

Working with generic constraints

I know this question has been asked before but I have no idea how to solve this current problem. I have defined a protocol MultipleChoiceQuestionable with an associatedtype property:
protocol Questionable {
var text: String {get set}
var givenAnswer: String? {get set}
}
protocol MultipleChoiceQuestionable: Questionable {
associatedtype Value
var answers: Value { get }
}
struct OpenQuestion: Questionable {
var text: String
var givenAnswer: String?
}
struct MultipleChoiceQuestion: MultipleChoiceQuestionable {
typealias Value = [String]
var text: String
var givenAnswer: String?
var answers: Value
}
struct NestedMultipleChoiceQuestion: MultipleChoiceQuestionable {
typealias Value = [MultipleChoiceQuestion]
var text: String
var answers: Value
var givenAnswer: String?
}
Types which conform to this protocol are saved in an array as Questionable like so:
// This array contains OpenQuestion, MultipleChoiceQuestion and NestedMultipleChoiceQuestion
private var questions: [Questionable] = QuestionBuilder.createQuestions()
Somewhere in my code I want to do something like:
let question = questions[index]
if let question = question as? MultipleChoiceQuestionable {
// Do something with the answers
question.answers = .....
}
This is not possible because Xcode warns me: Protocol MultipleChoiceQuestionable can only be used as a generic constraint. I've been searching around on how to solve this issue since generics are quite new for me. Apparently Swift doesn't know the type of the associatedtype during compile time which is the reason this error is thrown. I've read about using type erasure but I don't know if that solves my problem. Maybe I should use generic properties instead or perhaps my protocols are defined wrong?
If the action you want to apply to your sub-protocol objects does not rely on the associated type (i.e. neither has the a generic parameter nor returns the generic type) you can introduce an auxiliary protocol which just exposes the properties/methods you need, let your type conform to that protocol, and declare the question in terms of that protocol.
For example, if you just want to know some info about the question:
protocol MultipleChoiceInfo {
var numberOfAnswers: Int { get }
}
extension MultipleChoiceQuestion: MultipleChoiceInfo {
var numberOfAnswers: Int { return answers.count }
}
// do the same for the other multiple-choice types
Then you can access the questions through the new protocol like this:
let question = questions[index]
if let info = question as? MultipleChoiceInfo {
print(info.numberOfAnswers)
}
As I said, if you cannot provide an abstract (non-generic) interface then this won't work.
EDIT
If you need to process the generic data inside your questions you can extract the logic depending on the concrete generic type into another "processing" type which provides an interface to your questions. Each question type then dispatches its data to the processor interface:
protocol MultipleChoiceProcessor {
func process(stringAnswers: [String])
func process(nestedAnswers: [MultipleChoiceQuestion])
}
protocol MultipleChoiceProxy {
func apply(processor: MultipleChoiceProcessor)
}
extension MultipleChoiceQuestion: MultipleChoiceProxy {
func apply(processor: MultipleChoiceProcessor) {
processor.process(stringAnswers: answers)
}
}
Just create a type conforming to MultipleChoiceProcessor and do the type-check dance again:
if let proxy = question as? MultipleChoiceProxy {
proxy.apply(processor:myProcessor)
}
As an aside, if you don't have more protocols and structs in your real application, you might also just ditch the protocol stuff altogether... for this kind of problem it seems a bit over-engineered.