I am trying to pass a function to a SwiftUI view (from UIKit) using UIHostingController that is being called with a .openUrl closure but I don't think I am either passing or defining it correctly via UIKit view because if I use all SwiftUI the function will run.
I get the below error when attempting to run the function when it is passed to a UIHostingController containing a SwiftUI view from a UIKit UIViewController
[NSCFString countByEnumeratingWithState:objects:count:]: unrecognized selector sent to instance 0x60....
I have the URL "refresh" typed correctly and it matches the one defined in URL Types in my Target (see image).
I have also tried defining func action() outside of loadInitialScreen() and passed to UIHostingController -> MessageView as self.action() but this did not work either.
Why am I getting this error, Am I passing or defining the action() function in RootViewController incorrectly to UIHostingController or what???
UIKit View Controller
class RootViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.loadInitialScreen()
}
func loadInitialScreen() {
let messageViewController = UIHostingController(rootView:
MessageView(messageTitle: "Title", action: { action() })
)
func action() {
print("ACTION PRESSED")
}
self.navigationController?.pushViewController(messageViewController, animated: false)
}
}
SwiftUI View
struct MessageView: View {
let messageTitle: String
let action: () -> Void
var body: some View {
Button(action: {action()}, label: {
HStack {
Text("[\(messageTitle)](refresh://action)")
+ Text(" Text")
}.onOpenURL { _ in
action()
}
})
}
}
Related
I am working on a SwiftUI project, in my app i have to show a map, i am able to show map using UIViewRepresentable, now that map view will have a button and on tapping on that button mapview will needs to show controller (full screen direction) view and i know i can do this using UIViewControllerRepresentable.
I need help in which how can i show the UIViewControllerRepresentable from button click of UIViewRepresentable
Till now what i have done is, adding my code :
On button click i am calling showDetailMap function which calls the MapDetailViewController , but some how MapDetailViewController is not opening as model.
private func showDetailMap() {
MapDetailViewController()
}
struct MapDetailViewController: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> MapDetailViewController {
let viewController: MapDetailViewController = MapDetailViewController()
return viewController
}
func updateUIViewController(_ uiViewController: MapDetailViewController, context: Context) {}
}
Please help me with this.
struct ContentView: View {
#State var isPresentVC = false
var body: some View {
ZStack {
Button("OpenMapVC") {
isPresentVC = true
}
}.fullScreenCover(isPresented: $isPresentVC, content: {
MapDetailViewController()
})
}
}
I have a SwiftUI view
import SwiftUI
struct ContentView: View {
#State var list = [1,2,3,4]
var body: some View {
VStack {
Button("Click Me") {
list.append(list.last! + 1)
print(list)
}
List {
ForEach(list , id : \.self) { item in
Text("\(item)")
}
}
}
}
}
I have loaded that code inside my project using UIHostViewcontroller.
#IBAction func buttonAction(_ sender: Any) {
let contentViewController = UIHostingController(rootView: ContentView())
self.navigationController?.pushViewController(contentViewController, animated: true)
}
The list not updating when tapped on button and I have created new project with UIKit and loaded same ContentView from FirstViewController it's working fine, But I don't know what's wrong with existing projects.
I've come across some strange behavior with SwiftUI's onAppear() and onDisappear() events. I need to be able to reliably track when a view is visible to the user, disappears, and any other subsequent appear/disappear events (the use case is tracking impressions for mobile analytics).
I was hoping to leverage the onAppear() and onDisappear() events associated with swiftUI views, but I'm not seeing consistent behavior when using those events. The behavior can change depending on view modifiers as well as the simulator on which I run the app.
In the example code listed below, I would expect that when ItemListView2 appears, I would see the following printed out in the console:
button init
button appear
And on the iPhone 8 simulator, I see exactly that.
However, on an iPhone 12 simulator, I see:
button init
button appear
button disappear
button appear
Things get even weirder when I enable the listStyle view modifier:
button init
button appear
button disappear
button appear
button disappear
button appear
button appear
The iPhone 8, however remains consistent and produces the expected result.
I should also note that in no case, did the Button ever seem to disappear and re-appear to the eye.
These inconsistencies are also not simulator only issues, i noticed them on devices as well.
I need to reliably track these appear/disappear events. For example I'd need to know when a cell in a list appears (scrolled into view) or disappears (scrolled out of view) or when, say a user switches tabs.
Has anyone else noticed this behavior? To me this seems like a bug in SwiftUI, but I'm not certain as I've not used SwiftUI enough to trust myself to discern a programmer error from an SDK error. If any of you have noticed this, did you find a good work-around / fix?
Thanks,
Norm
// Sample code referenced in explanation
// Using Xcode Version 12.1 (12A7403) and iOS 14.1 for all simulators
import SwiftUI
struct ItemListView2: View {
let items = ["Cell 1", "Cell 2", "Cell 3", "Cell 4"]
var body: some View {
ListingView(items: items)
}
}
private struct ListingView: View {
let items: [String]
var body: some View {
List {
Section(
footer:
FooterButton()
.onAppear { print("button appear") }
.onDisappear { print("button disappear") }
) {
ForEach(items) { Text($0) }
}
}
// .listStyle(GroupedListStyle())
}
}
private struct FooterButton: View {
init() {
print("button init")
}
var body: some View {
Button(action: {}) { Text("Button") }
}
}
In SwiftUI you don't control when items in a List appear or disappear. The view graph is managed internally by SwiftUI and views may appear/disappear at any time.
You can, however, attach the onAppear / onDisappear modifiers to the outermost view:
List {
Section(footer: FooterButton()) {
ForEach(items, id: \.self) {
Text($0)
}
}
}
.listStyle(GroupedListStyle())
.onAppear { print("list appear") }
.onDisappear { print("list disappear") }
Try this UIKit approach. Similar behavior continues to exist under iOS 14.
protocol RemoteActionRepresentable: AnyObject {
func remoteAction()
}
struct UIKitAppear: UIViewControllerRepresentable {
let action: () -> Void
func makeUIViewController(context: Context) -> UIAppearViewController {
let vc = UIAppearViewController()
vc.delegate = context.coordinator
return vc
}
func updateUIViewController(_ controller: UIAppearViewController, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(action: self.action)
}
class Coordinator: RemoteActionRepresentable {
var action: () -> Void
init(action: #escaping () -> Void) {
self.action = action
}
func remoteAction() {
action()
}
}
}
class UIAppearViewController: UIViewController {
weak var delegate: RemoteActionRepresentable?
override func viewDidLoad() {
view.addSubview(UILabel())
}
override func viewDidAppear(_ animated: Bool) {
delegate?.remoteAction()
}
}
extension View {
func onUIKitAppear(_ perform: #escaping () -> Void) -> some View {
self.background(UIKitAppear(action: perform))
}
}
Example:
var body: some View {
MyView().onUIKitAppear {
print("UIViewController did appear")
}
}
I am trying to navigate to a View Controller while currently using NavigationLink from SwiftUI. I am assuming I am not able to do this, so I am wondering how I can navigate to my View Controller in another way while still being able to click on a button from my HomeView and navigate to my ViewController.
Below is my HomeView where I want a button with the text 'Time Sheets' to navigate to my ViewController
struct HomeView: View {
var body: some View {
NavigationView {
VStack {
Image("OELogo")
.resizable()
.frame(width: 400, height: 300)
NavigationLink(destination: ViewController()) {
Text("Time Sheets")
.fontWeight(.bold)
.frame(minWidth: 325, minHeight: 50)
.font(.title)
.foregroundColor(.gray)
.padding()
.overlay(
RoundedRectangle(cornerRadius: 50)
.stroke(Color.gray, lineWidth: 2)
)
}
Below is the start to my code of ViewController file that I want to navigate to
import UIKit
import KVKCalendar
import SwiftUI
final class ViewController: UIViewController {
private var events = [Event]()
private var selectDate: Date = {
let formatter = DateFormatter()
formatter.dateFormat = "dd.MM.yyyy"
return formatter.date(from: "27.7.2020") ?? Date()
}()```
To achieve this, you would need to create a new struct that conforms to UIViewControllerRepresentable. This acts like a wrapper for UIKits UIViewController.
There is a similar protocol for UIView, UIViewRepresentable.
struct YourViewControllerView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> ViewController {
// this will work if you are not using Storyboards at all.
return ViewController()
}
func updateUIViewController(_ uiViewController: ViewController, context: Context) {
// update code
}
}
Alternatively, this struct will work if you have your ViewController inside a storyboard.
struct YourViewControllerViewWithStoryboard: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> ViewController {
guard let viewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(identifier: "ViewController") as? ViewController else {
fatalError("ViewController not implemented in storyboard")
}
return viewController
}
func updateUIViewController(_ uiViewController: ViewController, context: Context) {
// update code
}
}
Remember to set the Restoration ID and Storyboard ID in the Interface builder if you are using a storyboard
I'm trying to find a way to trigger an action that will call a function in my UIView when a button gets tapped inside swiftUI.
Here's my setup:
foo()(UIView) needs to run when Button(SwiftUI) gets tapped
My custom UIView class making use of AVFoundation frameworks
class SomeView: UIView {
func foo() {}
}
To use my UIView inside swiftUI I have to wrap it in UIViewRepresentable
struct SomeViewRepresentable: UIViewRepresentable {
func makeUIView(context: Context) -> CaptureView {
SomeView()
}
func updateUIView(_ uiView: CaptureView, context: Context) {
}
}
SwiftUI View that hosts my UIView()
struct ContentView : View {
var body: some View {
VStack(alignment: .center, spacing: 24) {
SomeViewRepresentable()
.background(Color.gray)
HStack {
Button(action: {
print("SwiftUI: Button tapped")
// Call func in SomeView()
}) {
Text("Tap Here")
}
}
}
}
}
You can store an instance of your custom UIView in your representable struct (SomeViewRepresentable here) and call its methods on tap actions:
struct SomeViewRepresentable: UIViewRepresentable {
let someView = SomeView() // add this instance
func makeUIView(context: Context) -> SomeView { // changed your CaptureView to SomeView to make it compile
someView
}
func updateUIView(_ uiView: SomeView, context: Context) {
}
func callFoo() {
someView.foo()
}
}
And your view body will look like this:
let someView = SomeViewRepresentable()
var body: some View {
VStack(alignment: .center, spacing: 24) {
someView
.background(Color.gray)
HStack {
Button(action: {
print("SwiftUI: Button tapped")
// Call func in SomeView()
self.someView.callFoo()
}) {
Text("Tap Here")
}
}
}
}
To test it I added a print to the foo() method:
class SomeView: UIView {
func foo() {
print("foo called!")
}
}
Now tapping on your button will trigger foo() and the print statement will be shown.
M Reza's solution works for simple situations, however if your parent SwiftUI view has state changes, every time when it refreshes, it will cause your UIViewRepresentable to create new instance of UIView because of this: let someView = SomeView() // add this instance. Therefore someView.foo() is calling the action on the previous instance of SomeView you created, which is already outdated upon refreshing, so you might not see any updates of your UIViewRepresentable appear on your parent view.
See: https://medium.com/zendesk-engineering/swiftui-uiview-a-simple-mistake-b794bd8c5678
A better practice would be to avoid creating and referencing that instance of UIView when calling its function.
My adaption to M Reza's solution would be calling the function indirectly through parent view's state change, which triggers updateUIView :
var body: some View {
#State var buttonPressed: Bool = false
VStack(alignment: .center, spacing: 24) {
//pass in the #State variable which triggers actions in updateUIVIew
SomeViewRepresentable(buttonPressed: $buttonPressed)
.background(Color.gray)
HStack {
Button(action: {
buttonPressed = true
}) {
Text("Tap Here")
}
}
}
}
struct SomeViewRepresentable: UIViewRepresentable {
#Binding var buttonPressed: Bool
func makeUIView(context: Context) -> SomeView {
return SomeView()
}
//called every time buttonPressed is updated
func updateUIView(_ uiView: SomeView, context: Context) {
if buttonPressed {
//called on that instance of SomeView that you see in the parent view
uiView.foo()
buttonPressed = false
}
}
}
Here's another way to do it using a bridging class.
//SwiftUI
struct SomeView: View{
var bridge: BridgeStuff?
var body: some View{
Button("Click Me"){
bridge?.yo()
}
}
}
//UIKit or AppKit (use NS instead of UI)
class BridgeStuff{
var yo:() -> Void = {}
}
class YourViewController: UIViewController{
override func viewDidLoad(){
let bridge = BridgeStuff()
let view = UIHostingController(rootView: SomeView(bridge: bridge))
bridge.yo = { [weak self] in
print("Yo")
self?.howdy()
}
}
func howdy(){
print("Howdy")
}
}
Here is yet another solution! Communicate between the superview and the UIViewRepresentable using a closure:
struct ContentView: View {
/// This closure will be initialized in our subview
#State var closure: (() -> Void)?
var body: some View {
SomeViewRepresentable(closure: $closure)
Button("Tap here!") {
closure?()
}
}
}
Then initialize the closure in the UIViewRepresentable:
struct SomeViewRepresentable: UIViewRepresentable {
// This is the same closure that our superview will call
#Binding var closure: (() -> Void)?
func makeUIView(context: Context) -> UIView {
let uiView = UIView()
// Since `closure` is part of our state, we can only set it on the main thread
DispatchQueue.main.async {
closure = {
// Perform some action on our UIView
}
}
return uiView
}
}
#ada10086 has a great answer. Just thought I'd provide an alternative solution that would be more convenient if you want to send many different actions to your UIView.
The key is to use PassthroughSubject from Combine to send messages from the superview to the UIViewRepresentable.
struct ContentView: View {
/// This will act as a messenger to our subview
private var messenger = PassthroughSubject<String, Never>()
var body: some View {
SomeViewRepresentable(messenger: messenger) // Pass the messenger to our subview
Button("Tap here!") {
// Send a message
messenger.send("button-tapped")
}
}
}
Then we monitor the PassthroughSubject in our subview:
struct SomeViewRepresentable: UIViewRepresentable {
let messenger = PassthroughSubject<String, Never>()
#State private var subscriptions: Set<AnyCancellable> = []
func makeUIView(context: Context) -> UIView {
let uiView = UIView()
// This must be run on the main thread
DispatchQueue.main.async {
// Subscribe to messages
messenger.sink { message in
switch message {
// Call funcs in `uiView` depending on which message we received
}
}
.store(in: &subscriptions)
}
return uiView
}
}
This approach is nice because you can send any string to the subview, so you can design a whole messaging scheme.
My solution is to create an intermediary SomeViewModel object. The object stores an optional closure, which is assigned an action when SomeView is created.
struct ContentView: View {
// parent view holds the state object
#StateObject var someViewModel = SomeViewModel()
var body: some View {
VStack(alignment: .center, spacing: 24) {
SomeViewRepresentable(model: someViewModel)
.background(Color.gray)
HStack {
Button {
someViewModel.foo?()
} label: {
Text("Tap Here")
}
}
}
}
}
struct SomeViewRepresentable: UIViewRepresentable {
#ObservedObject var model: SomeViewModel
func makeUIView(context: Context) -> SomeView {
let someView = SomeView()
// we don't want the model to hold on to a reference to 'someView', so we capture it with the 'weak' keyword
model.foo = { [weak someView] in
someView?.foo()
}
return someView
}
func updateUIView(_ uiView: SomeView, context: Context) {
}
}
class SomeViewModel: ObservableObject {
var foo: (() -> Void)? = nil
}
Three benefits doing it this way:
We avoid the original problem that #ada10086 identified with #m-reza's solution; creating the view only within the makeUIView function, as per the guidance from Apple Docs, which state that we "must implement this method and use it to create your view object."
We avoid the problem that #orschaef identified with #ada10086's alternative solution; we're not modifying state during a view update.
By using ObservableObject for the model, we can add #Published properties to the model and communicate state changes from the UIView object. For instance, if SomeView uses KVO for some of its properties, we can create an observer that will update some #Published properties, which will be propagated to any interested SwiftUI views.