Error with initializers and super.init() in NSView Class - swiftui

I'm quite a newbie at SwitUI development and I've been almost a week trying to solve a problem but i'm really frustrated about.
The thing is very simple I guess. I just trying to send a #Binding var to a NSView class and print the value in my console on click in a SwiftUI View.
Code
This is my code:
import SwiftUI
class TapHandlerView: NSView {
   
  #Binding var text: String
   
  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
   
  init(text: Binding<String>) {
    self._text = text
    super.init(text: text) // Line error -> Argument passed to call that takes no arguments
  }
  override func mouseDown(with event: NSEvent) {
    super.mouseDown(with: event)
    print(text)
  }
}
struct TapHandler: NSViewRepresentable {
   
  #Binding var text: String
  func makeNSView(context: Context) -> TapHandlerView {
    TapHandlerView(text: $text)
  }
  func updateNSView(_ nsView: TapHandlerView, context: Context) {
  }
}
struct ItemView: View {
  #State var text: String = "Hello!"
  var body: some View {
    VStack {
     Text("Click here!")
    }
    .overlay(TapHandler(text: $text))
  }
}
_
This is the error I get:
super.init(text: text)
Argument passed to call that takes no arguments
_
For more context, I come from here:
SwiftUI ForEach's onMove() modifier stop working if the content view has tap gesture action
I hope that someone can help me and sorry if is a silly question, I feel like I'm missing something very basic but im blocked with it.
Thank you!

Related

How to make UIViewControllerRepresentable ViewController reusable in many SwiftUI views?

I have managed to create a UIKit UIAlertController and present it to my SwiftUI View. I had to create a UIKit UIAlertController so i can be able to change the colors of the cancel button and submit button as well as make the submit button bold. It worked but now i need to use the UIAlertController in other SwiftUI Views so i have to make it reusable.
How can i make UIViewControllerRepresentable ViewController reusable in many SwiftUI views without having to copy and past the code below? I am thinking of using a class and just instantiate the class and use the UIKit UIAlertController. Any help or small example appreciated.
This is the UIViewControllerRepresentable struct in my SwiftUI view. The code below has to be reusable without having to add it in each SwiftUI view.
struct UIAlertViewPopup: UIViewControllerRepresentable {
#Binding var show: Bool
let viewModel: SettingsViewModel
var title: String
var message: String
func makeUIViewController(context: UIViewControllerRepresentableContext<UIAlertViewPopup>) -> some UIViewController {
return UIViewController()
}
//update UIKit from SwiftUI
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<UIAlertViewPopup>) {
if (self.show) {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
let cancelAction = UIAlertAction(title: "Cancel", style: .destructive) { (action) in
self.show = false
}
let submitAction = UIAlertAction(title: "Edit profile", style: .default) { (action) in
viewModel(perform: .editProfile)
self.show = false
}
submitAction.setValue(UIColor.accentBlue, forKey: "titleTextColor")
cancelAction.setValue(UIColor.accentPrimary, forKey: "titleTextColor")
alert.addAction(cancelAction)
alert.addAction(submitAction)
alert.preferredAction = submitAction
DispatchQueue.main.async {
uiViewController.present(alert, animated: true, completion: {
self.show = false
})
}
}
}
//update SwiftUI from UIKit through delegates
func makeCoordinator() -> UIAlertViewPopup.Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UIAlertViewDelegate {
var control: UIAlertViewPopup
init(_ control: UIAlertViewPopup) {
self.control = control
}
}
}
I use it in my SwiftUI view like this
#State var isAlertVisible: Bool = false
Button {
isAlertVisible = true
}
then on the .background modifier
.background(UIAlertViewPopup(show: $viewModel.isAlertVisible, viewModel: viewModel, title: "", message: "hello from UIKit alert"))

Multiple NavigationLinks leading to UIViewControllerRepresentable destination ends up with blank screen

I have found a minimal SwiftUI app that exhibits a bug:
class HelloWorldVC: UIViewController {
override func loadView() {
super.loadView()
let label = UILabel()
label.text = "Hello, World"
view.addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
label.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
label.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
label.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
label.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
}
}
struct ViewControllerContainer: UIViewControllerRepresentable {
let vc: UIViewController
func makeUIViewController(context: Context) -> some UIViewController { vc }
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {}
}
struct ContentView: View {
var body: some View {
NavigationView {
// Works:
//NavigationLink("View UIKit VC", destination: ViewControllerContainer(vc: HelloWorldVC()))
// Only loads the UIKit view once. It's blank on subsequent tries.
NavigationLink(
"Detail Screen",
destination: NavigationLink(
"View UIKit VC",
destination: ViewControllerContainer(vc: HelloWorldVC())
)
)
}
}
}
Steps to reproduce:
Tap "Detail Screen"
Tap "View UIKit VC". You will see the "Hello, World" UIViewController.
Tap Back
Tap "View UIKit VC"
Expected:
You should see the "Hello, World" UIViewController again
Actual:
You will see a blank view. This will happen as many times as you try.
Note: In the commented out code, it works properly if you only have one layer deep of NavigationLink.
You are not working with UIViewControllerRepresentable correctly. You need to create a new view controller inside makeUIViewController, reusing it breaks the view controller lifecycle in your case.
The UIViewControllerRepresentable properties can be passed to the view controller when you create or update it, as follows:
struct ViewControllerContainer: UIViewControllerRepresentable {
let props: Int
func makeUIViewController(context: Context) -> some UIViewController {
HelloWorldVC(props: props)
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
uiViewController.props = props
}
}

SwiftUI, tvOS, UIViewControllerRepresentable and a focus issue

I'm trying to implement search functionality in the tvOS SwiftUI app. I'm using UISearchController as the most straight forward solution to do this. I've wrapped it inside SearchView which conforms to UIViewControllerRepresentable. The problem is, that it looks like the focus engine refuses to focus on a part of a view controller UI - UISearchBar that is wrapped. I can type the search query from my Mac, inside the simulator, to verify that the search works, but of course, it's not the real thing.
I've tried to add the .focusable() modifier into a SearchView, but it didn't help.
Also tried to implement shouldUpdateFocus, preferredFocusEnvironments and didUpdateFocus callbacks inside my custom subclasses of UISearchController and UISearchContainerViewController but those are not called at all.
I think I'm missing something very straightforward here.
Here is the code for the SearchView:
struct SearchView: UIViewControllerRepresentable {
#Binding var text: String
typealias UIViewControllerType = UISearchContainerViewController
typealias Context = UIViewControllerRepresentableContext<SearchView>
func makeUIViewController(context: Context) -> UIViewControllerType {
let controller = UISearchController(searchResultsController: context.coordinator)
controller.searchResultsUpdater = context.coordinator
return UISearchContainerViewController(searchController: controller)
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { }
func makeCoordinator() -> SearchView.Coordinator {
return Coordinator(text: $text)
}
class Coordinator: UIViewController, UISearchResultsUpdating {
#Binding var text: String
init(text: Binding<String>) {
_text = text
super.init(nibName: nil, bundle: nil)
}
func updateSearchResults(for searchController: UISearchController) {
guard let searchText = searchController.searchBar.text else { return }
text = searchText
}
}
}
And the main ContentView (I've stripped some non-important code):
struct ContentView: View {
#State var model = ["aa", "ab", "bb", "bc", "cc", "dd", "ee"]
#State var searchQuery: String = ""
var body: some View {
SearchView(text: $searchQuery)
List {
ForEach(model.filter({ $0.hasPrefix(searchQuery) })) { item in
Text(item)
}
}
}
}
Finally, it can be done through the .searchable modifier, introduced in iOS 15 beta 1, no need for UISearchController wrappers anymore.
P.S. Got an official response from Apple on this bug (FB8974300) to also use .searchable modifier, so there is no fix for the older versions.

SwiftUI changing text title of navigationBar Button at the current view to "Back" instead of inheriting the text from previous view title

I have a swiftUI view with navigation links that when i click will navigate to another view . The issue is the second view navigationBa button title still has the title of the previous view instead of a logical back title . How i can have the title as Back with changing the title as "Back" in the first view .
First view navigationBar code: The second view just shows the news website in a WebView.
.navigationBarTitle("Breaking News")
The way i tried is changing the title to this:
.navigationBarTitle("Back")
This will work but the title of the first view changes to "Back" Instead of "Breaking News"
Is there any way i can fix this
An alternative approach is to hide the back button and create your own back button like this:
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination : SomeView()) {
Text("Open")
}
.navigationBarTitle("Breaking News")
}
}
}
// Use navigationBarItems for creating your own bar item.
struct SomeView : View {
#Environment(\.presentationMode) var mode
var body : some View {
Text("Hello, World!")
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading:
Button(action : {
self.mode.wrappedValue.dismiss()
}){
Text("\(Image(systemName: "chevron.left"))Back")
})
}
}
The accepted answer looks glitchy, it removes a lot of standard behaviour and animations, including long press gesture.
Consider using custom backBarButtonTitle modifier:
struct FirstView: View {
var body: some View {
Text("First view")
.backBarButtonTitle("Back")
}
}
It sets the backButtonTitle property to the topmost UINavigationItem in stack. Be sure to use this modifier on the view, whose title you want to change, see documentation.
Here is the implementation of the backBarButtonTitle:
import SwiftUI
import UIKit
extension View {
func backBarButtonTitle(_ title: String) -> some View {
modifier(BackButtonModifier(title: title))
}
}
// MARK: - BackButtonModifier
struct BackButtonModifier: ViewModifier {
let title: String
func body(content: Content) -> some View {
content.background(BackButtonTitleView(title: title))
}
}
// MARK: - BackButtonTitleView
private struct BackButtonTitleView: UIViewRepresentable {
let title: String
func makeUIView(context _: Context) -> BackButtonTitleUIView {
BackButtonTitleUIView(title: title)
}
func updateUIView(_: BackButtonTitleUIView, context _: Context) {}
}
// MARK: - BackButtonTitleUIView
private final class BackButtonTitleUIView: UIView {
// MARK: Lifecycle
init(title: String) {
self.title = title
super.init(frame: .zero)
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: Internal
override func layoutSubviews() {
super.layoutSubviews()
if didConfigureTitle {
return
}
let topNavigationItem = searchNavigationController(currentResponder: self)?
.topViewController?
.navigationItem
if let topNavigationItem {
topNavigationItem.backButtonTitle = title
didConfigureTitle = true
}
}
// MARK: Private
private let title: String
private var didConfigureTitle = false
private func searchNavigationController(currentResponder: UIResponder) -> UINavigationController? {
if let navigationController = currentResponder as? UINavigationController {
return navigationController
} else if let nextResponder = currentResponder.next {
return searchNavigationController(currentResponder: nextResponder)
} else {
return nil
}
}
}

UITextView in a modal sheet is not working

To make UI-based editing of a NSAttributedString property (in a managed object) possible, a UITextView is used instead of a SwiftUI TextField View. The text view is located in a modal view being presented by a sheet function.
.sheet(isPresented: $presentSheet) { ...
(to illustrate and reproduce, the code below is a simplified version of this scenario)
The modal view is used to edit a selected model item that is shown in a list through a ForEach construct. The selected model item is passed as an #Observable object to the modal view.
When selecting an item "A", the modal view and the UITextView correctly shows this model item. If selecting a new item "B", the modal view correctly shows this "B" item. But if "B" is now being edited the change will affect the "A" object.
The reason for this behaviour is probably that the UIViewRepresentable view (representing the UITextView) is only initialised once. Further on from here, this seems to be caused by the way a sheet (modal) view is presented in SwiftUI (state variables are only initialised when the sheet first appear, but not the second time).
I am able to fix this malfunction by passing the selected item as a #Binding instead of an #Observable object, although I am not convinced that this is the right way to handle the situation, especially because everything works nicely, if a SwiftUI TextField is used instead of the UITextView (in the simplified case).
Worth mentioning, I seems to have figured out, what goes wrong in the case with the UITextView - without saying that this solves the problem.
In the code listed below (which repro the problem), the Coordinator's init function has one assignment that initialises the Coordinator with the parent. Since this is value and not a reference assignment, and since the Coordinator only get initialised once, an edit of the UITextView will likely access a wrong parent.
Again, I am not certain about my solution to the problem, is the right one, since everything works fine when using a SwiftUI TextField instead. I therefore hope to see some comments on this problem.
struct ContentView: View {
var states = [StringState("A"), StringState("B"), StringState("C"), StringState("D"), StringState("E")]
#State var presentSheet = false
#State var state = StringState("A")
var body: some View {
VStack {
Text("state = \(state.s)")
ForEach(states) { s in
Button(action: {
self.state = s
self.presentSheet.toggle()
})
{
Text("\(s.s)")
}
}
}
.sheet(isPresented: $presentSheet) {
EditView(state: self.state, presentSheet: self.$presentSheet)
}
}
}
struct EditView: View
{
#ObservedObject var state: StringState
#Binding var presentSheet: Bool
var body: some View {
VStack {
Text("\(state.s)")
TextView(string: $state.s) // Edit Not OK
TextField("", text: $state.s ) // Edit OK
Button(action: {
self.presentSheet.toggle()
})
{ Text("Back") }
}
}
}
struct TextView: UIViewRepresentable
{
#Binding var string: String
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UITextView
{
let textview = UITextView(frame: CGRect.zero)
textview.delegate = context.coordinator
return textview
}
func updateUIView(_ uiView: UITextView, context: Context)
{
uiView.text = string
}
class Coordinator : NSObject, UITextViewDelegate
{
var parent: TextView
init(_ textView: TextView) {
self.parent = textView
}
func textViewDidChange(_ textView: UITextView)
{
self.parent.string = textView.text!
}
}
}
class StringState: Identifiable, ObservableObject
{
let ID = UUID()
var s: String
init(_ s : String) {
self.s = s
}
}
A couple of changes will fix it:
func updateUIView(_ uiView: UITextView, context: Context)
{
uiView.text = string
context.coordinator.parent = self
}
And also add #Published to your ObservableObject:
class StringState: Identifiable, ObservableObject
{
let ID = UUID()
#Published var s: String
init(_ s : String) {
self.s = s
}
}