I've been experimenting on trying to binding subclass to a SwiftUI view that takes it parent class as a parameter.
These are my classes:
class Animal {
func sound() -> String {
return ""
}
}
class Cat : Animal {
override func sound() -> String {
return "Meow"
}
func purr() {
print("purring")
}
}
class Dog : Animal {
override func sound() -> String {
return "Woof"
}
func fetch() {
print("fetching")
}
}
Here are the views I have set up.
struct ContentView: View {
#State var creature:Cat = Cat()
var body: some View {
AnimalView(creature: $creature)
}
}
struct AnimalView: View {
#Binding var creature:Animal
var body: some View {
Text(creature.sound())
.padding()
}
}
This results in the compile error:
Cannot convert value of type 'Binding<Cat>' to expected argument type 'Binding<Animal>'
What the proper way to do bind to a view the takes a parent class?
Searching around I think the structure I would want is to make the view itself generic.
struct ContentView: View {
#State var creature:Cat = Cat()
#State var creature2:Dog = Dog()
var body: some View {
VStack {
AnimalView(creature: $creature)
AnimalView(creature: $creature2)
}
}
}
struct AnimalView<T:Animal> : View {
#Binding var creature:T
#State var sound:String = "No Sound"
var body: some View {
Text(creature.name)
.padding()
.onAppear {
self.sound = self.creature.sound()
}
}
}
This will allow me to bind to types that inherit from Animal and let me use one view rather that having to create a separate Cat and Dog view.
Also just in case for better form, here is a same thing with protocol and classes.
protocol Animal {
var name:String {
get
}
func sound() -> String
}
struct Cat : Animal {
var name:String {
get {
return "Cat"
}
}
func sound() -> String {
return "Meow"
}
func purr() {
print("purring")
}
}
struct Dog : Animal {
var name:String {
get {
return "Dog"
}
}
func sound() -> String {
return "Woof"
}
func fetch() {
print("fetching")
}
}
Related
I'm trying to extend a protocol so that a certain few impls of the protocol have a view associated with them. However, because a SwiftUI View is a protocol, this is proving to be a challenge.
import SwiftUI
protocol ParentProtocol {
var anyProperty: String { get }
}
protocol ChildProtocol : ParentProtocol {
associatedtype V
var someView: V { get }
}
class ChildImpl : ChildProtocol {
var someView : some View {
Text("Hello World")
}
var anyProperty: String = ""
}
class ChildMgr {
var child: ParentProtocol = ChildImpl()
func getView() -> some View {
guard let child = child as? ChildProtocol else { return EmptyView() }
return child.someView
}
}
Its not clear to me where to constrain the ChildProtocol's associated type to a View (or Text for that matter).
At the guard let child = ... I get the following compiler error:
Protocol 'ChildProtocol' can only be used as a generic constraint because it has Self or associated type requirements
and when returning the chid's view I get:
Member 'someView' cannot be used on value of protocol type 'ChildProtocol'; use a generic constraint instead
I think the answer may be in this thread: https://developer.apple.com/forums/thread/7350
but frankly its confusing on how to apply it to this situation.
Don't use runtime checks. Use constrained extensions.
I also don't see a reason for you to be using classes.
protocol ChildProtocol: ParentProtocol {
associatedtype View: SwiftUI.View
var someView: View { get }
}
final class ChildImpl: ChildProtocol {
var someView: some View {
Text("Hello World")
}
var anyProperty: String = ""
}
final class ChildMgr<Child: ParentProtocol> {
var child: Child
init(child: Child) {
self.child = child
}
}
extension ChildMgr where Child: ChildProtocol {
func getView() -> some View {
child.someView
}
}
extension ChildMgr {
func getView() -> some View {
EmptyView()
}
}
extension ChildMgr where Child == ChildImpl {
convenience init() {
self.init(child: .init())
}
}
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")
}
}
I have an AsyncContentView that handles the loading of data when the view appears and handles the switching of a loading view and the content (Taken from here swiftbysundell):
struct AsyncContentView<P:Parsable, Source:Loader<P>, Content: View>: View {
#ObservedObject private var source: Source
private var content: (P.ReturnType) -> Content
init?(source: Source, reloadAfter reloadTime:UInt64 = 0, #ViewBuilder content: #escaping (P.ReturnType) -> Content) {
self.source = source
self.content = content
}
func loadInfo() {
Task {
await source.loadData()
}
}
var body: some View {
switch source.state {
case .idle:
return AnyView(Color.clear.onAppear(perform: loadInfo))
case .loading:
return AnyView(ProgressView("Loading..."))
case .loaded(let output):
return AnyView(content(output))
}
}
}
For completeness, here's the Parsable protocol:
protocol Parsable: ObservableObject {
associatedtype ReturnType
init()
var result: ReturnType { get }
}
And the LoadingState and Loader
enum LoadingState<Value> {
case idle
case loading
case loaded(Value)
}
#MainActor
class Loader<P:Parsable>: ObservableObject {
#Published public var state: LoadingState<P.ReturnType> = .idle
func loadData() async {
self.state = .loading
await Task.sleep(2_000_000_000)
self.state = .loaded(P().result)
}
}
Here is some dummy data I am using:
struct Interface: Hashable {
let name:String
}
struct Interfaces {
let interfaces: [Interface] = [
Interface(name: "test1"),
Interface(name: "test2"),
Interface(name: "test3")
]
var selectedInterface: Interface { interfaces.randomElement()! }
}
Now I put it all together like this which does it's job. It processes the async function which shows the loading view for 2 seconds, then produces the content view using the supplied data:
struct ContentView: View {
class SomeParsableData: Parsable {
typealias ReturnType = Interfaces
required init() { }
var result = Interfaces()
}
#StateObject var pageLoader: Loader<SomeParsableData> = Loader()
#State private var selectedInterface: Interface?
var body: some View {
AsyncContentView(source: pageLoader) { result in
Picker(selection: $selectedInterface, label: Text("Selected radio")) {
ForEach(result.interfaces, id: \.self) {
Text($0.name)
}
}
.pickerStyle(.segmented)
}
}
}
Now the problem I am having, is this data contains which segment should be selected. In my real app, this is a web request to fetch data that includes which segment is selected.
So how can I have this view update the selectedInterface #state property?
If I simply add the line
self.selectedInterface = result.selectedInterface
into my AsyncContentView I get this error
Type '()' cannot conform to 'View'
You can do it in onAppear of generated content, but I suppose it is better to do it not directly but via binding (which is like a reference to state's external storage), like
var body: some View {
let selected = self.$selectedInterface
AsyncContentView(source: pageLoader) { result in
Picker(selection: selected, label: Text("Selected radio")) {
ForEach(result.interfaces, id: \.self) {
Text($0.name).tag(Optional($0)) // << here !!
}
}
.pickerStyle(.segmented)
.onAppear {
selected.wrappedValue = result.selectedInterface // << here !!
}
}
}
I have an example struct:
public struct Axis: Hashable, CustomStringConvertible {
public var name: String
public var description: String {
return "Axis: \"\(name)\""
}
}
And property wrapper to make some operations on [Axis] struct.
#propertyWrapper
struct WrappedAxes {
var wrappedValue: [Axis] {
// This is just example, in real world it's much more complicated.
didSet {
for index in wrappedValue.indices {
var elems = Array(wrappedValue[index].name.split(separator: " "))
if elems.count>1 {
elems.removeLast()
}
let new = elems.reduce(into:"", {$0 += "\($1) "})
wrappedValue[index].name = new+("\(Date())")
} } } }
And I try to add, insert and remove Axes in SwiftUI View:
public struct ContentView: View {
#Binding var axes: [Axis]
public var body: some View {
VStack {
ForEach(axes.indices, id:\.self) {index in
HStack {
TextField("", text: $axes[index].name)
Button("Delete", action: {deleteAxis(index)})
Button("Insert", action: {insertAxis(index)})
}
}
Button("Add", action: addAxis)
}
}
var addAxis: () -> Void {
return {
axes.append(Axis(name: "New"))
print (axes)
}
}
var deleteAxis: (_:Int)->Void {
return {
if $0 < axes.count {
axes.remove(at: $0)
}
print (axes)
}
}
var insertAxis: (_:Int)->Void {
return {
if $0 < axes.count {
axes.insert(Axis(name: "Inserted"), at: $0)
}
print (axes)
}
}
public init (axes: Binding<[Axis]>) {
self._axes = axes
}
}
As far, as print (axes) shows changes are made, View never updates. I made very small App to test in which I call ContentView:
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
#WrappedAxes var axes = [Axis(name: "FirstOne")]
func applicationDidFinishLaunching(_ aNotification: Notification) {
let contentView = ContentView(
axes: Binding (
get: {self.axes},
set: { [self] in axes = $0}))
.... // No fancy stuff
I'm open for all critique of code itself, and help: how to push this view (and all possible future subviews) to update when axes changed?
The thing is that #Binding is for nested Views. When you want to make changes in a SwiftUI view, you have to use the #State instead in the one where the action starts. Plus, you don't need to set an initialiser this way. You can set your value like this, inside an ObservableObject to handle your logic:
struct Axis {
var name: String
var description: String {
return "Axis: \"\(name)\""
}
}
final class AxisViewModel: ObservableObject {
#Published var axes: [Axis] = [Axis(name: "First")]
init() { handleAxes() }
func addAxis() {
axes.append(Axis(name: "New"))
handleAxes()
}
func insertAxis(at index: Int) {
axes.insert(Axis(name: "Inserted"), at: index)
handleAxes()
}
func handleAxes() {
for index in axes.indices {
var elems = Array(axes[index].name.split(separator: " "))
if elems.count > 1 {
elems.removeLast()
}
let new = elems.reduce(into:"", { $0 += "\($1) " })
axes[index].name = new + ("\(Date())")
}
}
}
struct ContentView: View {
#ObservedObject var viewModel = AxisViewModel()
var body: some View {
VStack {
ForEach(viewModel.axes.indices, id:\.self) { index in
HStack {
TextField("", text: $viewModel.axes[index].name)
Button("Insert", action: { viewModel.insertAxis(at: index) })
}
}
Button("Add", action: viewModel.addAxis)
}
}
}
I made a new struct Axes:
public struct Axes {
#WrappedAxes var _axes: [Axis]
public subscript (index: Int) -> Axis {
get { return _axes[index]}
set { _axes[index] = newValue}
}
// and all needed functions and vars to simulate Array behaviour:
public mutating func append(_ newElement: Axis ) {
_axes.append(newElement)
}
public mutating func insert(_ newElement: Axis, at index: Int ) {
_axes.insert(newElement, at: index)
}
public mutating func remove(at index: Int ) {
_axes.remove(at: index)
}
....
}
and put it into #ObservalbeObject:
public class Globals: ObservableObject {
#Published public var axes: Axes
....
}
Then defined AxesView as
public struct AxesView: View {
#ObservedObject var globals: Globals
public var body: some View {
...
}
...
}
That's all. For a while it works.
I want to delete an object which is marked as #ObjectBinding, in order to clean up some TextFields for example.
I tried to set the object reference to nil, but it didn't work.
import SwiftUI
import Combine
class A: BindableObject {
var didChange = PassthroughSubject<Void, Never>()
var text = "" { didSet { didChange.send() } }
}
class B {
var property = "asdf"
}
struct DetailView : View {
#ObjectBinding var myObject: A = A() //#ObjectBinding var myObject: A? = A() -> Gives an error.
#State var mySecondObject: B? = B()
var body: some View {
VStack {
TextField($myObject.text, placeholder: Text("Enter some text"))
Button(action: {
self.test()
}) {
Text("Clean up")
}
}
}
func test() {
//myObject = nil
mySecondObject = nil
}
}
If I try to use an optional with #ObjectBinding, I'm getting the Error
"Cannot convert the value of type 'ObjectBinding' to specified type
'A?'".
It just works with #State.
Regards
You can do something like this:
class A: BindableObject {
var didChange = PassthroughSubject<Void, Never>()
var form = FormData() { didSet { didChange.send() } }
struct FormData {
var firstname = ""
var lastname = ""
}
func cleanup() {
form = FormData()
}
}
struct DetailView : View {
#ObjectBinding var myObject: A = A()
var body: some View {
VStack {
TextField($myObject.form.firstname, placeholder: Text("Enter firstname"))
TextField($myObject.form.lastname, placeholder: Text("Enter lastname"))
Button(action: {
self.myObject.cleanup()
}) {
Text("Clean up")
}
}
}
}
I absolutely agree with #kontiki , but you should remember to don't use #State when variable can get outside. #ObjectBinding right way in this case. Also all new way of memory management already include optional(weak) if they need it.
Check this to get more information about memory management in SwiftUI
Thats how to use #ObjectBinding
struct DetailView : View {
#ObjectBinding var myObject: A
and
DetailView(myObject: A())