Navigating to a SwiftUI view from a button pressed in a UIViewControllerRepresentable - swiftui

I am using a Swiftui list to show tasks. Each task has a location. When you click on the list row I am using a NavigationLink to navigate to a details page.
I am using a UIViewControllerRepresentable that creates a map view with all the task locations annotated on the map. I had to use UIKit so that you can interact with the annotations. I have set up a delegate that fires when the user clicks the map annotation accessory.
The aim: when the delegate function is called I want to pass back the taskid which is used to navigate to the details page like when the list row is clicked on:
The way I am Navigating from the list:
ForEach(tasksToDisplayInList, id: \.id) { task in
NavigationLink(
destination: TaskDetailsView(taskId: task.id, title: task.title))
{
TaskListRow(task: task)
.padding(.trailing, 5)
.foregroundColor(.textDefault)
}
}
The Task List Map UIViewControllerRepresentable that passes back the pressed task id:
struct TaskListMap: UIViewControllerRepresentable {
typealias UIViewControllerType = TaskListMapController
#Binding var annotations: [TaskAnnotation]
class Coordinator: TaskListMapDelegate {
func didPressMoreDetail(_ mapController: TaskListMapController, for annotation: TaskAnnotation) {
// TaskAnnotation contains the taskId
// Navigate here somehow?
}
var parent: TaskListMap
init(_ parent: TaskListMap) {
self.parent = parent
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> TaskListMapController {
let mapController = TaskListMapController(annotations: annotations)
mapController.delegate = context.coordinator
return mapController
}
func updateUIViewController(_ uiViewController: TaskListMapController, context: Context) {
if annotations.count != uiViewController.mapView.annotations.count {
uiViewController.mapView.removeAnnotations(uiViewController.mapView.annotations)
uiViewController.mapView.addAnnotations(annotations)
}
}
}

As a workaround, this seems to work fine for my purpose:
// # TaskListMapView.swift (inside the coordinator)
class Coordinator: TaskListMapDelegate {
var parent: TaskListMap
init(_ parent: TaskListMap) {
self.parent = parent
}
func didPressMoreDetail(_ mapController: TaskListMapController, for annotation: TaskAnnotation) {
// Push the navigation with a hosting controller
let detailsView = UIHostingController(rootView: TaskDetailsView(taskId: annotation.taskId, title: annotation.title ?? ""))
mapController.navigationController?.pushViewController(detailsView, animated: true)
}
}

Related

Remove title when pushing EKCalendarChooser to Navigation Stack with SwiftUI

I'm working on an app where I want to push the EKCalendarChooser View Controller to the navigation stack with a navigation link. Everything works as expected apart from the fact that I can't get rid of some magic title/label.
I want to hide the title marked with the red rectangle in the image.
I'm using the following code to push the view:
NavigationLink(destination: CalendarChooser(eventStore: self.eventStore)
.edgesIgnoringSafeArea([.top,.bottom])
.navigationTitle("My Navigation Title")) {
Text("Calendar Selection")
}
And this is my UIViewControllerRepresentable
import SwiftUI
import EventKitUI
struct CalendarChooser: UIViewControllerRepresentable {
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
#Environment(\.presentationMode) var presentationMode
let eventStore: EKEventStore
func makeUIViewController(context: UIViewControllerRepresentableContext<CalendarChooser>) -> UINavigationController {
let chooser = EKCalendarChooser(selectionStyle: .multiple, displayStyle: .allCalendars, entityType: .event, eventStore: eventStore)
chooser.selectedCalendars = Set(eventStore.selectableCalendarsFromSettings)
chooser.delegate = context.coordinator
chooser.showsDoneButton = false
chooser.showsCancelButton = false
return UINavigationController(rootViewController: chooser)
}
func updateUIViewController(_ uiViewController: UINavigationController, context: UIViewControllerRepresentableContext<CalendarChooser>) {
}
class Coordinator: NSObject, UINavigationControllerDelegate, EKCalendarChooserDelegate {
var parent: CalendarChooser
init(_ parent: CalendarChooser) {
self.parent = parent
}
func calendarChooserDidFinish(_ calendarChooser: EKCalendarChooser) {
let selectedCalendarIDs = calendarChooser.selectedCalendars.compactMap { $0.calendarIdentifier }
UserDefaults.savedCalendarIDs = selectedCalendarIDs
NotificationCenter.default.post(name: .calendarSelectionDidChange, object: nil)
parent.presentationMode.wrappedValue.dismiss()
}
func calendarChooserDidCancel(_ calendarChooser: EKCalendarChooser) {
parent.presentationMode.wrappedValue.dismiss()
}
}
}
Note that I'm not even sure that I'm on the right track here and I'm open for any solution.
I think I've found a solution to my own problem. With a small modification
to my UIViewControllerRepresentable the view looks the way I want it to. More specifically to the updateUIViewController function:
func updateUIViewController(_ uiViewController: UINavigationController, context: UIViewControllerRepresentableContext<CalendarChooser>) {
uiViewController.setNavigationBarHidden(true, animated: false) // This line!
}
By doing this I keep the navigation controls and title from the navigation link, which looks like this:

Updating #State var from UIKit

I have a SwiftUI that shows two items in a List view. I want to be able push the desired list entry from UIKit. For this purpose, I have created an #State var which I want to set from UIKit to trigger the desired list entry via the binding on the NavigationLink's selection parameter.
public struct DateOptions: View, MiscellaneousHelper {
#State var selectedItem: AppMainFeatureData? = nil
let tabBarController: TabBarController
public var body: some View {
NavigationView {
List(AppMainFeatureData.dateItems, id: \.self) { item in
NavigationLink(destination: DateFeatureController(svc: tabBarController.svc, feature: item),
tag: item, selection: $selectedItem) {
ImageText(text: item.title, imageName: item.iconName)
}
}
.navigationBarTitle(SideBarSection.dates.title)
}
}
}
Here's my UIViewControllerRepresentable:
struct DateFeatureController: UIViewControllerRepresentable, MiscellaneousHelper {
let svc: SplitViewController
let feature: AppMainFeatureData
func makeUIViewController(context: Context) -> UINavigationController {
UINavigationController(rootViewController: feature.viewController)
}
func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {
}
}
The is how I create DateOptions in my UITabBar and how it gets added to the viewControllers:
let dateViewController = UIHostingController(rootView: DateOptions(tabBarController: self))
dateViewController.tabBarItem = tabBarItem
viewControllers?.insert(dateViewController, at: 1)
Here's how I set selectedItem from a UITabBar:
if let dateFeature = viewControllers?[selectedIndex] as? UIHostingController<DateOptions> {
dateFeature.rootView.selectedItem = .dateAddSubtract
dPrint("After \(dateFeature.rootView.selectedItem)")
}
But the print shows selectedItem as nil.
Everything works fine when the the user touches a List entry.
How can I set selectedItem from UIKit?

How to add a customized InfoWindow to markers in google-maps swift ui?

i tried to make a view like bellow in SwiftUi without any success Customized info window swift ui
Since this question doesn't have too much detail, I will be going off of some assumptions. First, I am assuming that you are calling the MapView through a UIViewControllerRepresentable.
I am not too familiar with the Google Maps SDK, but this is possible through the GMSMapViewDelegate Methods. After implementing the proper GMSMapViewDelegate method, you can use ZStacks to present the image that you would like to show.
For example:
struct MapView: UIViewControllerRepresentable {
var parentView: ContentView
func makeUIViewController(context: Context) {
let mapView = GMSMapView()
return mapView
}
func updateUIViewController(_ uiViewController: GMSMapView, context: Context) {
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
class Coordinator: NSObject, GMSMapViewDelegate {
var parent: MapView
init(_ parent: MapView) {
self.parent = parent
}
//Use the proper Google Maps Delegate method to find out if a marker was tapped and then show the image by doing: parent.parentView.isShowingInformationImage = true.
}
}
In your SwiftUI view that you would like to put this MapView in, you can do the following:
struct ContentView: View {
#State var isShowingInformationImage = false
var body: some View {
ZStack {
if isShowingInformationImage {
//Call the View containing the image
}
MapView(parentView: self)
}
}
}

UITabViewController in SwiftUI has its Tab's OnAppear Being Called Twice

I'm currently making use UITabBarController in SwiftUI. Here is the implementation:
struct MyTabView: View {
private var viewControllers: [UIHostingController<AnyView>]
public init( _ views: [AnyView]) {
self.viewControllers = views.map { UIHostingController(rootView:$0) }
}
public var body: some View {
return TabBarController(controllers: viewControllers)
.edgesIgnoringSafeArea(.all)
}
}
struct TabBarController: UIViewControllerRepresentable {
var controllers: [UIViewController]
func makeUIViewController(context: Context) -> UITabBarController {
let tabBarController = UITabBarController()
tabBarController.viewControllers = controllers
return tabBarController
}
func updateUIViewController(_ tabBarController: UITabBarController, context: Context) { }
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITabBarControllerDelegate {
var parent: TabBarController
init(_ tabBarController: TabBarController) {
self.parent = tabBarController
}
}
}
Inside of my SwiftUI I have the following:
struct ContentView: View {
var body: some View {
MyTabView([
AnyView(Text("Moo Moo")),
AnyView(MyPage())
])
}
}
struct MyPage:View {
var body:some View {
NavigationView {
VStack {
ForEach((1...10).reversed(), id: \.self) { value -> AnyView in
print("For Each Value Called")
return AnyView(MyView(text: String(value)))
}
}
}
}
}
struct MyView:View {
let text:String
var body:some View {
Text(text).onAppear {
print("On Appear Called - Making Service Call for \(text)")
}
}
}
I have the following questions:
When running this code the On Appear Called - Making Service Call for \(text), is called twice. What would cause this? My expectation is that it is only run once. Should this be occurring?
Is this a SwiftUI bug lurking around or is this expected behaviour?
Yes, your expectation would be correct. However, it looks like a bug.
The problem appear when having content inside NavigationView. If you use .onAppear() on the NavigationView, you will see it called only once. If you use onAppear() on the VStack, it's already twice.
This has reported in this thread aswell
From my view, this behavior is wrong. Maybe report to Apple or ask why
maybe I found a solution:
add on every very first NavigationLink the modifier .isDetailLink(false)
for me it stops the double onAppear calls

Make UIKit ContextMenu preview provider frame fit a SwiftUI view inside a UIHostingControler

I am learning Swift & SwiftUI as a hobby with no UIKit background, so I am not sure if this is currently possible. I would really like to use UIKit's context menus with SwiftUI (e.g. to implement submenus, action attributes and maybe custom preview providers).
My original idea was to create a LegacyContextMenuView with UIViewControllerRepresentable. Then I'd use a UIHostingController to add a SwiftUI View as a child of a UIViewController ContainerViewController to which I'd add a UIContextMenuInteraction.
My current solution kinda works but when the context menu is activated the preview frame of the 'ContainerViewController' view does not fit the size of UIHostingController view. I am not familiar with UIKit's layout system so I'd like to know:
Is it possible to add such constrains while the preview is activated?
Is it possible to preserve the clipShape of the underlying SwiftUI view inside the preview provider?
The code:
// MARK: - Describes a UIKit Context Menu
struct LegacyContextMenu {
let title: String
let actions: [UIAction]
var actionProvider: UIContextMenuActionProvider {
{ _ in
UIMenu(title: title, children: actions)
}
}
init(actions: [UIAction], title: String = "") {
self.actions = actions
self.title = title
}
}
// MARK: - A View that brings UIKit context menus into the SwiftUI world
struct LegacyContextMenuView<Content: View>: UIViewControllerRepresentable {
let content: Content
let menu: LegacyContextMenu
func makeUIViewController(context: Context) -> UIViewController {
let controller = ContainerViewController(rootView: content)
let menuInteraction = UIContextMenuInteraction(delegate: context.coordinator)
controller.view.addInteraction(menuInteraction)
return controller
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) { }
func makeCoordinator() -> Coordinator { Coordinator(parent: self) }
class Coordinator: NSObject, UIContextMenuInteractionDelegate {
let parent: LegacyContextMenuView
init(parent: LegacyContextMenuView) { self.parent = parent }
func contextMenuInteraction(
_ interaction: UIContextMenuInteraction,
configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration?
{
// previewProvider nil = using the default UIViewController: ContainerViewController
UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: parent.menu.actionProvider)
}
}
class ContainerViewController: UIViewController {
let hostingController: UIHostingController<Content>
init(rootView: Content) {
self.hostingController = UIHostingController(rootView: rootView)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func viewDidLoad() {
super.viewDidLoad()
setupHostingController()
setupContraints()
// Additional setup required?
}
func setupHostingController() {
addChild(hostingController)
view.addSubview(hostingController.view)
hostingController.didMove(toParent: self)
}
// Not familiar with UIKit's layout system so unsure if this is the best approach
func setupContraints() {
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
view.addConstraints([
hostingController.view.topAnchor.constraint(equalTo: view.topAnchor),
hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
hostingController.view.leftAnchor.constraint(equalTo: view.leftAnchor),
hostingController.view.rightAnchor.constraint(equalTo: view.rightAnchor)
])
}
}
}
// MARK: - Simulate SwiftUI syntax
extension View {
func contextMenu(_ legacyContextMenu: LegacyContextMenu) -> some View {
self.modifier(LegacyContextViewModifier(menu: legacyContextMenu))
}
}
struct LegacyContextViewModifier: ViewModifier {
let menu: LegacyContextMenu
func body(content: Content) -> some View {
LegacyContextMenuView(content: content, menu: menu)
}
}
Then to test, I use this:
// MARK - A sample view with custom content shape and a dynamic frame
struct SampleView: View {
#State private var isLarge = false
let viewClipShape = RoundedRectangle(cornerRadius: 50.0)
var body: some View {
ZStack {
Color.blue
Text(isLarge ? "Large" : "Small")
.foregroundColor(.white)
.font(.largeTitle)
}
.onTapGesture { isLarge.toggle() }
.clipShape(viewClipShape)
.contentShape(viewClipShape)
.frame(height: isLarge ? 250 : 150)
.animation(.easeInOut, value: isLarge)
}
}
struct ContentView: View {
var body: some View {
SampleView()
.contextMenu(LegacyContextMenu(actions: [sampleAction], title: "My Menu"))
.padding(.horizontal)
}
let sampleAction = UIAction(
title: "Remove",
image: UIImage(systemName: "trash.fill"),
identifier: nil,
attributes: UIMenuElement.Attributes.destructive,
handler: { _ in print("Pressed 'Remove'") })
}
While long pressing, the context menu scaling animation respects the content shape of SampleView for both small and large sizes, but the preview pops out like this:
you must set preferredContentSize of ViewController to fit content size u want