Having troubles putting down together SwiftUI and generic types for handling Core Data.
Consider following example:
Parent is abstract. Foo and Bar are children of Parent and they have some custom attributes.
Now what I want to do, is roughly that:
protocol EntityWithView {
associatedtype T: View
func buildView() -> T
}
extension Parent: EntityWithView {
func buildView() -> some View {
fatalError("Re-implement in child")
}
}
extension Foo {
override func buildView() -> some View {
return Text(footribute)
}
}
extension Bar {
override func buildView() -> some View {
return Text(atrribar)
}
}
struct ViewThatUsesCoreDataAsModel: View {
let entities: [Parent]
var body: some View {
ForEach(entities) { entity in
entity.buildView()
}
}
}
I would want to add polymorphic builder to my core data entities that shape data or build views, that confirm to common interface so I can use them without casting/typing.
Problem that compiler throws errors if I try to modify generated Core data entity directly not through extension, and confirming to protocol though extension doesn't allow overriding.
Ok, this is head-breaking (at least for Preview, which gone crazy), but it works in run-time. Tested with Xcode 11.4 / iOS 13.4.
As we need to do all in extension the idea is to use dispatching via Obj-C messaging, the one actually available pass to override implementation under such requirements.
Note: use Simulator or Device
Complete test module
protocol EntityWithView {
associatedtype T: View
var buildView: T { get }
}
extension Parent {
// allows to use Objective-C run-time messaging by complete
// type erasing.
// By convention subclasses
#objc func generateView() -> Any {
AnyView(EmptyView()) // << safe
//fatalError("stub in base") // << alternate
}
}
extension Parent: EntityWithView {
var buildView: some View {
// restory SwiftUI view type from dispatched message
guard let view = self.generateView() as? AnyView else {
fatalError("Dev error - subview must generate AnyView")
}
return view
}
}
extension Foo {
#objc override func generateView() -> Any {
AnyView(Text(footribute ?? ""))
}
}
extension Bar {
#objc override func generateView() -> Any {
AnyView(Text(attribar ?? ""))
}
}
struct ViewThatUsesCoreDataAsModel: View {
let entities: [Parent]
var body: some View {
VStack {
ForEach(entities, id: \.self) { entity in
entity.buildView
}
}
}
}
struct DemoGeneratingViewInCoreDataExtension: View {
#Environment(\.managedObjectContext) var context
var body: some View {
ViewThatUsesCoreDataAsModel(entities: [
Foo(context: context),
Bar(context: context)
])
}
}
Related
Im trying to write boiler plate code for an MVVM architecture and I'm trying to make the View interface inside my ViewModel instead of binding properties between them.
I do get a compiler error saying Type 'V.Command' has no member 'reload' How can I improve my code so that View is abstracted inside my ViewModel?
Interfaces
protocol Actionable {
associatedtype Command
func perform(command: Command)
}
extension Actionable {
func perform(command: Command) {}
}
protocol Rendering {
associatedtype Model
func render(with model: Model)
}
extension Rendering {
func render(with dependencies: Model) {}
}
protocol VMView: Rendering, Actionable {}
protocol ViewModel {
associatedtype Command
associatedtype Dependencies
associatedtype Model
}
View Implementation
final class NewViewVC: UIViewController, VMView {
typealias Model = NewViewVM<NewViewVC>.Model
typealias Command = NewViewVM<NewViewVC>.Command
func perform(command: Command) {
print("performing \(command)")
}
func render(with model: Model) {
print("rendering \(model)")
}
var viewModel: NewViewVM<NewViewVC>!
override func viewDidLoad() {
super.viewDidLoad()
}
}
extension NewViewVC {
static func create(dependencies: NewViewVM<NewViewVC>.Dependencies) -> NewViewVC {
let vc = NewViewVC()
let viewModel = NewViewVM.create(dependencies: dependencies, services: .init(), view: vc)
vc.viewModel = viewModel
return vc
}
static func createMock(dependencies: NewViewVM<NewViewVC>.Dependencies, services: NewViewVM<NewViewVC>.Dependencies.Services) -> NewViewVC {
let vc = NewViewVC()
let viewModel = NewViewVM.create(dependencies: dependencies, services: services, view: vc)
vc.viewModel = viewModel
return vc
}
}
ViewModel Implementation
protocol NewViewVMInterface {
func reloadData()
}
final class NewViewVM<V: VMView>: ViewModel, NewViewVMInterface {
struct Model {}
enum Command {
case start, reload
}
struct Dependencies {
struct Services {
}
}
private var view: V!
private var dependencies: Dependencies!
private var services: Dependencies.Services!
static func create(dependencies: Dependencies, services: Dependencies.Services, view: V) -> NewViewVM
where V.Model == Model, V.Command == Command {
let viewModel = NewViewVM()
viewModel.dependencies = dependencies
viewModel.services = services
viewModel.view = view
return viewModel
}
func reloadData() {
// fetch data
view.perform(command: .reload) // <- Where error happens
}
}
Thanks for any help. A quick fix I can find is to force cast the command to V.Command but that's very ugly. I feel there is a better solution.
Looks like if I put the NewViewVMInterface conformance to an extension I'll be able to do something like this:
extension NewViewVM: NewViewVMInterface where V.Command == Command, V.Model == Model {
func reloadData() {
view.perform(command: .reload)
}
}
However in my past experiences I've noticed sometime when making SDKs having protocol conformances in extensions results in unexpected behaviours. Im still open to any other suggestions out there.
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 building a camera app with all the UI in SwiftUI (parent) holding a UIKit Controller that contains all the recording functionalities. The UI is pretty complex, so would like if possible to remain with this structure for the project.
The UIKit Class has some functions like startRecord() stopRecord() which I would like to be triggered from the SwiftUI view. For that reason, I would like to 'call' the UIKit functions from my SwiftUI view.
I am experimenting with UIViewControllerRepresentable, being able to perform updates on a global variable change, but I am still not able to call the individual functions I want to trigger from the SwiftUI parent.
Here its the SwiftUI file:
init(metalView: MetalViewController?) {
self.metalView = MetalViewController(appStatus: appStatus)
}
var body: some View {
ZStack {
// - Camera view
metalView
.edgesIgnoringSafeArea(.top)
.padding(.bottom, 54)
VStack {
LateralMenuView(appStatus: appStatus, filterTooltipShowing: $_filterTooltipShowing)
Button("RECORD", action: {
print("record button pressed")
metalView?.myMetalDelegate.switchRecording(). // <-- Not sure about this
})
Here is the MetalViewController:
protocol MetalViewControllerDelegate {
func switchRecording()
}
// MARK: - The secret sauce for loading the MetalView (UIKit -> SwiftUI)
struct MetalViewController: UIViewControllerRepresentable {
var appStatus: AppStatus
typealias UIViewControllerType = MetalController
var myMetalDelegate: MetalViewControllerDelegate!
func makeCoordinator() -> Coordinator {
Coordinator(metalViewController: self)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<MetalViewController>) -> MetalController {
let controller = MetalController(appStatus: appStatus)
return controller
}
func updateUIViewController(_ controller: MetalController, context: UIViewControllerRepresentableContext<MetalViewController>) {
controller.changeFilter()
}
class Coordinator: NSObject, MetalViewControllerDelegate {
var controller: MetalViewController
init(metalViewController: MetalViewController) {
controller = metalViewController
}
func switchRecording() {
print("just testing")
}
}
}
and the UIKit Controller...
class MetalController: UIViewController {
var _mydelegate: MetalViewControllerDelegate?
...
override func viewDidLoad() {
...
self._mydelegate = self
}
extension MetalController: MetalViewControllerDelegate {
func switchRecording() {
print("THIS SHOULD BE WORKING, BUT ITS NOT")
}
}
I like to use Combine to pass messages through an ObservableObject to the UIKit views. That way, I can call them imperatively. Rather than trying to parse your code, I made a little example of the concept:
import SwiftUI
import Combine
enum MessageBridgeMessage {
case myMessage(parameter: Int)
}
class MessageBridge : ObservableObject {
#Published var result = 0
var messagePassthrough = PassthroughSubject<MessageBridgeMessage, Never>()
}
struct ContentView : View {
#StateObject private var messageBridge = MessageBridge()
var body: some View {
VStack {
Text("Result: \(messageBridge.result)")
Button("Add 2") {
messageBridge.messagePassthrough.send(.myMessage(parameter: messageBridge.result))
}
VCRepresented(messageBridge: messageBridge)
}
}
}
struct VCRepresented : UIViewControllerRepresentable {
var messageBridge : MessageBridge
func makeUIViewController(context: Context) -> CustomVC {
let vc = CustomVC()
context.coordinator.connect(vc: vc, bridge: messageBridge)
return vc
}
func updateUIViewController(_ uiViewController: CustomVC, context: Context) {
}
func makeCoordinator() -> Coordinator {
return Coordinator()
}
class Coordinator {
private var cancellable : AnyCancellable?
func connect(vc: CustomVC, bridge: MessageBridge) {
cancellable = bridge.messagePassthrough.sink(receiveValue: { (message) in
switch message {
case .myMessage(let parameter):
bridge.result = vc.addTwo(input: parameter)
}
})
}
}
}
class CustomVC : UIViewController {
func addTwo(input: Int) -> Int {
return input + 2
}
}
In the example, MessageBridge has a PassthroughSubject that can be subscribed to from the UIKit view (or in this case, UIViewController). It's owned by ContentView and passed by parameter to VCRepresented.
In VCRepresented, there's a method on the Coordinator to subscribe to the publisher (messagePassthrough) and act on the messages. You can pass parameters via the associated properties on the enum (MessageBridgeMessage). Return values can be stored on #Published properties on the MessageBridge if you need them (or, you could setup another publisher to go the opposite direction).
It's a little verbose, but seems to be a pretty solid pattern for communication to any level of the tree you need (SwiftUI view, representable view, UIKit view, etc).
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
I have two reusable views, Ex1 and Ex2. I am trying to show one of them depends on a condition alternately but I could not it.
ContentvIew:
struct ContentView: View {
#State var selector = false
var cvc = ContentViewController()
var body: some View {
ZStack { // ERROR: Protocol type 'Any' cannot conform to 'View' because only concrete types can conform to protocols
cvc.getView(t: selector)
Button(action: {
self.selector.toggle()
print(self.selector)
}) {
Text("Button")
}
}
}
}
Ex1 :
import SwiftUI
struct Ex1: View {
var body: some View {
Text("Ex 1")
}
}
Ex2 :
import SwiftUI
struct Ex2: View {
var body: some View {
Text("Ex 2")
}
}
ContentViewController :
import Foundation
class ContentViewController {
let a = Ex1()
let b = Ex2()
func getView (t: Bool) ->(Any){
if t {
return a
}
else {
return b
}
}
}
I think it is very simple but not for me, for now. Help for two things, please.
I want to understand this problem, and the solution.
Best way for alternate two view in a layout.
Thanks in advance.
As the error suggests the return type specified in ContentViewController's getView method does not conform to the protocols.
In SwiftUI everything you specified in body{} clause must be a type of View if you do not know what kind of view available at runtime.
You can specify AnyView type for unknown views.
So your error will be removed by changing the ContentViewController's code.
class ContentViewController {
let a = Ex1()
let b = Ex2()
func getView (t: Bool) -> (AnyView) {
if t {
return AnyView(a)
}
else {
return AnyView(b)
}
}
}