I want to set value to EnvironmentObject from Delegate class.
struct AppleMapView: UIViewRepresentable {
#EnvironmentObject var mapViewViewModel: MapViewViewModel
let mapViewDelegate = MapViewDelegate()
class MapViewDelegate: NSObject, MKMapViewDelegate {
func mapView(_ mapView: MKMapView, didChange mode: MKUserTrackingMode, animated: Bool) {
// This is where I want to set value to EnvObj
**mapViewViewModel.mode = mode**
}
}
}
This is what I want to do.
My code gives error
Instance member 'mapViewViewModel' of type 'AppleMapView' cannot be used on instance of nested type 'AppleMapView.MapViewDelegate'
So, I've tried giving reference to delegate class:
MapViewDelegate(vm: mapViewViewModel)
This has no compile error, but when I run the code it made errors
A View.environmentObject(_:) for MapViewViewModel may be missing as an ancestor of this view.: file /BuildRoot/Library/Caches/com.apple.xbs/Sources/Monoceros_Sim/Monoceros-39.4.3/Core/EnvironmentObject.swift, line 55 ```
Neither works. How can I fix my code?
It works in different way, it needs to make your MapViewDelegate as coordinator for AppleMapView, like below
struct AppleMapView: UIViewRepresentable {
#EnvironmentObject var mapViewViewModel: MapViewViewModel
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.delegate = context.coordinator // << your delegate
return mapView
}
func makeCoordinator() -> MapViewDelegate {
MapViewDelegate(self) // << will be created for you
}
class MapViewDelegate: NSObject, MKMapViewDelegate {
var owner: AppleMapView
init(_ owner: AppleMapView) {
self.owner = owner
}
func mapView(_ mapView: MKMapView, didChange mode: MKUserTrackingMode, animated: Bool) {
owner.mapViewViewModel.mode = mode // << now you have access to owner props
}
}
}
Related
I am using MapView this way (the code is of course simplified):
struct IkMapView: UIViewRepresentable {
var mapView = MKMapView()
func makeUIView(context: Context) -> MKMapView {
mapView.delegate = context.coordinator
return mapView
}
public func unselectAllSpots() {
// mapView is useless and not the actual map view I am seeing
for annotation in mapView.annotations {
mapView.deselectAnnotation(annotation, animated: false)
}
}
func updateUIView(_ view: MKMapView, context: Context) {
// this view is good
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, MKMapViewDelegate, UIGestureRecognizerDelegate {
var parent: IkMapView
init(_ parent: IkMapView) {
self.parent = parent
super.init()
}
}
}
I would like to expose a few public functions to allow actions from the parent view, for example, here, it is the unselectAllSpots() function. However, when I call it from the parent, the mapView is not the same one as the one I get through updateUIView(), and it doesn't actually impact the map. I have read somewhere that the actual displayed mapView is not that instance (but the instance we get in updateUIView().
What's the best way to solve this? I could transform my struct into a class and create a new MKMapView property which I would associate to the map view I get through updateUIView but that doesn't feel right at all.
Thanks a lot for your help :)
A representable is updated on changed external states, so a possible approach is to do needed changes on binding changes, like
struct IkMapView: UIViewRepresentable {
#Binding var unselectAll: Bool // << activates update
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView() // create here !!
mapView.delegate = context.coordinator
return mapView
}
func updateUIView(_ mapView: MKMapView, context: Context) {
if unselectAll { // << handle state
for annotation in mapView.annotations {
mapView.deselectAnnotation(annotation, animated: false)
}
unselectAll = false
}
}
// .. other code
I have a UIKit ViewController that's nested inside a SwiftUI view using ViewControllerRepresentable. The SwiftUI view manages a bit of state (an Int, in this example) that I want to display in the UIKit view. When the user taps a button in the SwiftUI parent view, the state change should be reflected in the UIKit view. I've tried using the #Binding property wrapper to keep the two in sync, but clearly I'm missing something, as my view controller's initialiser throws a compile-time error.
I'm quite new to iOS development so perhaps I'm going in the complete wrong direction here. Any help would be greatly appreciated.
The code is as follows (simplified):
struct ContentView: View {
#State private var currentNumber: Int
init(currentNumber: Int) {
self.currentNumber = currentNumber
}
var body: some View {
FancyLabelViewControllerRepresentable(currentNumber: self.$currentNumber)
Button("Increment") {
self.currentNumber += 1
}
}
}
struct FancyLabelViewControllerRepresentable: UIViewControllerRepresentable {
typealias UIViewControllerType = FancyLabelViewController
#Binding var currentNumber: Int
init(currentNumber: Binding<Int>) {
self._currentNumber = currentNumber
}
func makeUIViewController(context: Context) -> FancyLabelViewController {
let fancyLabel = FancyLabelViewController(number: self.currentNumber)
fancyLabel.currentNumberInLabel = self.currentNumber
return fancyLabel
}
func updateUIViewController(_ uiViewController: FancyLabelViewController, context: Context) {
uiViewController.currentNumberInLabel = self.currentNumber
}
}
class FancyLabelViewController: UIViewController {
var label = UILabel()
#Binding var currentNumberInLabel: Int
init(number: Int) {
// Error: 'self' used in property access 'currentNumberInLabel' before 'super.init' call
self.currentNumberInLabel = number
// Error: Property 'self.currentNumberInLabel' not initialized at super.init call
super.init(nibName: nil, bundle: nil)
}
required init(coder: NSCoder) {
fatalError("Not implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
label.text = "\(currentNumberInLabel)"
view = label
}
}
I think you don't need the
#Binding var currentNumberInLabel: Int
because the UIViewControllerRepresentable already takes care of updating the currentNumberInLabel value, but you also needs to update the
label.text = "\(currentNumberInLabel)"
So I did something like
class FancyLabelViewController: UIViewController {
var label = UILabel()
var currentNumberInLabel: Int
init(number: Int) {
self.currentNumberInLabel = number
super.init(nibName: nil, bundle: nil)
}
required init(coder: NSCoder) {
fatalError("Not implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
label.text = "\(currentNumberInLabel)"
view = label
}
func updateLabel() {
label.text = "\(currentNumberInLabel)"
}
}
and call updateLabel from UIViewControllerRepresentable as
func updateUIViewController(_ uiViewController: FancyLabelViewController, context: Context) {
uiViewController.currentNumberInLabel = self.currentNumber
uiViewController.updateLabel()
}
In SwiftUI if you are transitioning using a NavigationLink() into a UIViewControllerRepresentable how would you; say, add buttons or change the title property on the navigationbar.
This is what I am doing right now:
import SwiftUI
/// Controls the actual action performed by the button upon taps.
struct CatagoryButton: View {
#State var isPresenting :Bool = false
var company : Company?
var text : String
var body: some View {
NavigationLink(destination: UIKitWrapper(company: self.company, storyboardPointer: self.text)
.navigationBarTitle(self.text)
.edgesIgnoringSafeArea(.all),
isActive: self.$isPresenting,
label: {
Button(action: {
self.isPresenting.toggle()
}){
ZStack {
ButtonShadowLayer(text: text)
GradientBackground()
.mask(ButtonBaseLayer())
CircleAndTextLayer(text: text)
}
}
})
}
}
Here is the struct for my representable.
import SwiftUI
/// Wraps UIKIT instance in a representable that swiftUI can present.
struct UIKitWrapper: UIViewControllerRepresentable {
//Specify what type of controller is being wrapped in an associated type.
typealias UIViewControllerType = UIViewController
//Company property passed from parent view. Represents the company the user selected from main view.
private var company : Company
//Determines which viewcontroller will be presented to user. This string corresponds to the name of the storyboard file in the main bundle.
private var storyboardPointer : String
init(company: Company?, storyboardPointer: String) {
guard let company = company else {fatalError()}
self.company = company
self.storyboardPointer = storyboardPointer
}
func makeUIViewController(context: Context) -> UIViewControllerType {
//Find user defined storyboard in bundle using name.
let storyboard = UIStoryboard(name: storyboardPointer, bundle: .main)
//Downcast returned controller to protocol AccessControllerProtocol. This step is required because we are not sure which storyboard will be accessed. Potential storyboard controllers that can be called all conform to this protocol.
//FIXME: Remove fatalError and create error enum asap.
guard let viewController = storyboard.instantiateInitialViewController() as? AccessControllerProtocol else { fatalError() }
//Assign user selected company object to instance property on incoming viewController.
viewController.company = company
//Return UINavigationController with storyboard instance view controller as root controller.
return viewController
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
}
Finally, here is one of the classes that use the representable.
import UIKit
class OrdersViewController: UIViewController, AccessControllerProtocol {
var company : Company!
#IBOutlet var companyNameLabel : UILabel!
override func viewDidLoad() {
super.viewDidLoad()
setBackgroundColor()
companyNameLabel.text = company.name
self.navigationController?.navigationItem.rightBarButtonItems = [UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(self.tapRightBarButton))]
}
func setBackgroundColor(){
let backgroundGradient = BackgroundGradientSetter()
let viewWithGradient = backgroundGradient.setGradientToView(with: [DarkBlueHue_DEFAULT,LightBlueHue_DEFAULT], size: view.bounds)
view.addSubview(viewWithGradient)
view.sendSubviewToBack(viewWithGradient)
}
#objc func tapRightBarButton(){
}
}
No matter what I do this button doesn't show up. I'm not sure if I need to put this in a makeCoordinator() or if there is just something I am missing. If anyone has insight please let me know!
If it isn't available in viewDidLoad, try calling your setupNavigation() in viewWillAppear()
In your case navigationController is not available yet on viewDidLoad, try instead as in below demo module
Tested & works with Xcode 11.2 / iOS 13.2
class MyUIController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.navigationController?.navigationBar.topItem?.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(self.onAdd(_:)))
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// might be needed to remove injected item here
}
#objc func onAdd(_ sender: Any?) {
print(">> tapped add")
}
}
struct MyInjector: UIViewControllerRepresentable {
func makeUIViewController(context: UIViewControllerRepresentableContext<MyInjector>) -> MyUIController {
MyUIController()
}
func updateUIViewController(_ uiViewController: MyUIController, context: UIViewControllerRepresentableContext<MyInjector>) {
}
}
struct DemoNavigationBarUIButton: View {
var body: some View {
NavigationView {
MyInjector()
.navigationBarTitle("Demo")
}
}
}
struct DemoNavigationBarUIButton_Previews: PreviewProvider {
static var previews: some View {
DemoNavigationBarUIButton()
}
}
I created a custom tile service and add it to a mapView then it works fine. then I used the same code and created a framework library with a MapView return type in Xcode 8. then I used a sample test app and import that library to it and I called the method used in library and add it to a mapView. So my problem Is when I call and that method to mapView it displays the MapKit map not my custom map
code used in library
import Foundation
import MapKit
public class mapLib: NSObject{
public class func createMap(mapView: MKMapView) ->MKMapView{
let mapView = mapView
//custom map URL
let template = "http://tile.openstreetmap.org/{z}/{x}/{y}.png"
let overlay = MKTileOverlay(urlTemplate: template)
overlay.canReplaceMapContent = true
mapView.add(overlay, level: .aboveLabels)
return mapView;
}
}
code used in app
import UIKit
import MapKit
import mapLib
class ViewController: UIViewController {
#IBOutlet weak var mapV: MKMapView!
override func viewDidLoad() {
super.viewDidLoad()
let view = mapLib.createMap(mapView: mapV)
mapV.addOverlays(view.overlays)
//any additional setup after loading the view, typically from a nib.
}
I need to clarify that the way I'm going to approach would work or any other method to do it :)
You are missing add self as delegate and implement func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer method, you can do this in your library
something like this
import Foundation
import MapKit
public class mapLib: NSObject{
public class func createMap(mapView: MKMapView) ->MKMapView{
let mapView = mapView
//custom map URL
let template = "http://tile.openstreetmap.org/{z}/{x}/{y}.png"
let overlay = MKTileOverlay(urlTemplate: template)
overlay.canReplaceMapContent = true
mapView.add(overlay, level: .aboveLabels)
mapView.delegate = self
return mapView;
}
}
extension mapLib : MKMapViewDelegate{
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if let overlayTile = overlay as? MKTileOverlay{
let overLayRenderer = MKTileOverlayRenderer(tileOverlay: overlayTile)
return overLayRenderer
}
return MKOverlayRenderer(overlay: overlay)
}
}
I want to remove repetitive code so I would like to create a simple MVP base view controller that will tie together a model, view and presenter types and automatically connect them e.g.:
class BaseMvpViewController<M: MvpModel, V: MvpView, P: MvpPresenter>: UIViewController {
Where my model and view are empty protocols:
protocol MvpModel {}
protocol MvpView: class {} // class is needed for weak property
and presenter looks like this:
protocol MvpPresenter {
associatedtype View: MvpView
weak var view: View? { get set }
func onAttach(view: View)
func onDetach(view: View)
}
This is my whole BaseMvpViewController:
class BaseMvpViewController<M: MvpModel, V, P: MvpPresenter>: UIViewController, MvpView {
typealias View = V
var model: M? = nil
var presenter: P!
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
deinit {
presenter.onDetach(view: self as! View)
}
override func viewDidLoad() {
createPresenter()
super.viewDidLoad()
presenter.onAttach(view: self as! View)
}
func createPresenter() {
guard presenter != nil else {
preconditionFailure("Presenter was not created or it was not assigned into the `presenter` property!")
}
}
}
The problem is that the V must be without the protocol i.e. cannot be V: MvpView. Otherwise specific implementation of a VC must have a class/struct and not just a protocol for the MvpView. All my views are just protocols and my VCs will implement them e.g.
class MyViewController: BaseMvpViewController<MyModel, MyView, MyPresenter>, MyView
Now the compiler complains in the onAttach() and onDetach() methods that "argument type 'V' does not conform to expected type 'MvpView'"
So I tried an extension:
extension BaseMvpViewController where V: MvpView {
override func viewDidLoad() {
presenter.onAttach(view: self as! View)
}
}
yet another compiler error: "cannot invoke 'onAttach' with an argument list of type '(view: V)'". There is another small compilation error "Members of constrained extensions cannot be declared #objc" where I override func viewDidLoad() in the extension. This can be fixed by my own method and calling that one from viewDidLoad in the custom class. Any idea how to achieve what I want?
This is a similar/same issue like Using some protocol as a concrete type conforming to another protocol is not supported but maybe something has been improved in the Swift world since then. Or did I really hit a hard limit in the current Swift's capabilities?
In have finally found a solution, the problem was in casting self as! View, it must be self as! P.View. And there cannot be a base protocol for view because protocols do not conform to themselves in Swift. Here is my complete code:
protocol MvpPresenter {
associatedtype View
var view: View? { get set }
var isAttached: Bool { get }
func onAttach(view: View)
func onDetach(view: View)
}
/// Default implementation for the `isAttached()` method just checks if the `view` is non nil.
extension MvpPresenter {
var isAttached: Bool { return view != nil }
}
class BaseMvpViewController<M, V, P: MvpPresenter>: UIViewController {
typealias View = V
var viewModel: M? = nil
private(set) var presenter: P!
//MARK: - Initializers
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override public init(nibName: String?, bundle: Bundle?) {
super.init(nibName: nibName, bundle: bundle)
}
deinit {
presenter.onDetach(view: self as! P.View)
}
//MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
presenter = createPresenter()
}
override func viewWillAppear(_ animated: Bool) {
guard let view = self as? P.View else {
preconditionFailure("MVP ViewController must implement the view protocol `\(View.self)`!")
}
super.viewWillAppear(animated)
if (!presenter.isAttached) {
presenter.onAttach(view: view)
}
}
//MARK: - MVP
/// Override and return a presenter in a subclass.
func createPresenter() -> P {
preconditionFailure("MVP method `createPresenter()` must be override in a subclass and do not call `super.createPresenter()`!")
}
}
And a sample VC:
class MyGenericViewController: BaseMvpViewController<MyModel, MyView, MyPresenter>, MyView {
...
override func createPresenter() -> MainPresenter {
return MyPresenter()
}
...
}
This VC will automatically have a viewModel property of type MyModel (could be anything e.g. struct, class, enum, etc), property presenter of type MyPresenter and this presenter will be automatically attached between viewDidLoad and viewWillAppear. One method must be overridden, the createPresenter() where you must create and return a presenter. This is called before the custom VC's viewDidLoad method. Presenter is detached in the deinit.
The last problem is that generic view controllers cannot be used in interface builder (IB), because IB talks to code via Objective-C runtime and that does not know true generics, thus does not see our generic VC. The app crashes when instantiating a generic VC from a storyboard/xib. There is a trick though that fixes this. Just load the generic VC manually into the Objective-C runtime before any instantiation from storyboard/xib. Good is in AppDelegate's init method:
init() {
...
MyGenericViewController.load()
...
}
EDIT 1:
I have found the loading of generic VC into Objective-C runtime in this SO answer https://stackoverflow.com/a/43896830/671580
EDIT 2:
Sample presenter class. The mandatory things is the typealias, the weak var view: View? and the onAttach & onDetach methods. Minimum implementation of the attach/detach methods is also provided.
class SamplePresenter: MvpPresenter {
// These two are needed!
typealias View = SampleView
weak var view: View?
private let object: SomeObject
private let dao: SomeDao
//MARK: - Initializers
/// Sample init method which accepts some parameters.
init(someObject id: String, someDao dao: SomeDao) {
guard let object = dao.getObject(id: id) else {
preconditionFailure("Object does not exist!")
}
self.object = object
self.dao = dao
}
//MARK: - MVP. Both the onAttach and onDetach must assign the self.view property!
func onAttach(view: View) {
self.view = view
}
func onDetach(view: View) {
self.view = nil
}
//MARK: - Public interface
/// Sample public method that can be called from the view (e.g. a ViewController)
/// that will load some data and tell the view to display them.
func loadData() {
guard let view = view else {
return
}
let items = dao.getItem(forObject: object)
view.showItems(items)
}
//MARK: - Private
}