Delegate for SwiftUI View from Hosting View Controller - ios

I add UIHostingViewController to my Storyboard and make class for this.
Load mySwiftUIView in controller init. Everything works good.
But i want to make hosting controller like delegate for mySwiftUIView because I want to handle button pressing in view.
required init?(coder: NSCoder) {
var mySwiftView = MySwiftUIView()
mySwiftView.delegate = self //self used before super.init call
super.init(coder: coder,rootView: mySwiftView)
}
Id doesn't work because I pass view before hosting controller fully init. Swift shows message "self used before super.init call"
If I will use usual UIViewController and put HostingController inside - it is works. But this is not suitable for me because it calls problem with sizes and navigation.
How can I do it with separate UIHostingController. Thanks
Full code
import UIKit
import SwiftUI
protocol MySwiftUIViewDelegate{
func buttonPressed()
}
struct MySwiftUIView: View {
var delegate: MySwiftUIViewDelegate?
var body: some View {
Button(action: {
delegate?.buttonPressed()
}) {
Text("My button")
}
}
}
class MyHostingViewController: UIHostingController<MySwiftUIView>, MySwiftUIViewDelegate {
required init?(coder: NSCoder) {
var mySwiftView = MySwiftUIView()
//mySwiftView.delegate = self //self used before super.init call
super.init(coder: coder,rootView: mySwiftView)
}
override func viewDidLoad() {
super.viewDidLoad()
}
func buttonPressed() {
performSegue(withIdentifier: "GoToOtherScreenSegue", sender: self)
}
}

Since MyHostingViewController knows that its rootView is MySwiftUIView, you could assign the view controller as the delegate afterwards:
required init?(coder: NSCoder) {
var mySwiftView = MySwiftUIView()
super.init(coder: coder, rootView: mySwiftView)
rootView.delegate = self
}

You can wrap delegate into class
import UIKit
import SwiftUI
protocol MySwiftUIViewDelegate{
func buttonPressed()
}
struct MySwiftUIView: View {
let configuration: Configuration
var body: some View {
Button(action: {
configuration.delegate?.buttonPressed()
}) {
Text("My button")
}
}
}
extension MySwiftUIView {
final class Configuration {
unowned var delegate: MySwiftUIViewDelegate?
}
}
class MyHostingViewController: UIHostingController<MySwiftUIView>, MySwiftUIViewDelegate {
required init?(coder: NSCoder) {
let configuration = MySwiftUIView.Configuration()
let mySwiftView = MySwiftUIView(configuration: configuration)
super.init(coder: coder,rootView: mySwiftView)
configuration.delegate = self
}
override func viewDidLoad() {
super.viewDidLoad()
}
func buttonPressed() {
performSegue(withIdentifier: "GoToOtherScreenSegue", sender: self)
}
}

Related

How do I implement data binding in a View Controller in SwiftUI?

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()
}

SwiftUI preview fails when using UIHostingController when creating UIHostingController outside viewDidLoad

I have some code that causes SwiftUI previews to fail but will run successfully in the simulator and on a device. The SwiftUI previews failure diagnostics message is:
PreviewUpdateTimedOutError: Updating took more than 5 seconds
Updating a preview from ViewControllerPreviews in NestedTest.app (9218) took more than 5 seconds.
I have boiled the issue down to a minimal amount of code:
import SwiftUI
import UIKit
let createHostInInit = true
class ViewController: UIViewController {
private var host: TestViewHost? = nil
init() {
if createHostInInit {
host = TestViewHost()
}
super.init(nibName: nil, bundle: nil)
}
// Note: this is here to satisfy the compiler and to allow running this
// in the simulator as part of a default new project.
required init?(coder: NSCoder) {
if createHostInInit {
host = TestViewHost()
}
super.init(coder: coder)
}
override func viewDidLoad() {
super.viewDidLoad()
if !createHostInInit {
host = TestViewHost()
}
if let host = self.host {
addChild(host)
self.view.addSubview(host.view)
host.didMove(toParent: self)
host.view.frame = self.view.bounds
}
}
}
class TestViewHost: UIHostingController<TestView> {
init() {
super.init(rootView: TestView())
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
struct TestView: View {
var body: some View {
Text("Hi there!")
}
}
struct UIViewControllerPreviewer: UIViewControllerRepresentable {
let viewController: UIViewController
init(viewController: UIViewController) {
self.viewController = viewController
}
func makeUIViewController(context: Context) -> some UIViewController {
return viewController
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
// Do nothing because this is only for previews
}
}
struct ViewControllerPreviews: PreviewProvider {
static var previews: some View {
return UIViewControllerPreviewer(viewController: ViewController())
}
}
If createHostInInit = true then the previews fail. If createHostInInit = false then the previews work. In either of these cases the UI looks correct in the simulator or on a device. So it would seem that something about the preview environment gets cranky about when a UIHostingController is created before viewDidLoad().
Did I miss some documentation for UIHostingController describing these limitations? Is this a bug?
This can be tested in Xcode by creating a new iOS single view project and dropping this code into ViewController.swift.
Thanks

On awakeFromNib() - Error Instance member 'button' cannot be used on type 'CustomView'

I've created a custom UIView as a .xib file with the UIView having a single button. I load the UIView using the below code.
struct ContentView: View {
var body: some View {
CustomViewRepresentable()
}
}
struct CustomViewRepresentable: UIViewRepresentable {
typealias UIViewType = CustomView
func makeUIView(context: UIViewRepresentableContext<CustomViewRepresentable>) -> CustomView {
let customView = Bundle.main.loadNibNamed("CustomView", owner: nil, options: nil)![0] as! CustomView
return customView
}
func updateUIView(_ uiView: CustomView, context: UIViewRepresentableContext<CustomViewRepresentable>) {
}
}
The custom view has the below code:
class CustomView: UIView {
#IBOutlet weak var button: UIButton!
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override class func awakeFromNib() {
super.awakeFromNib()
// Error - Instance member 'button' cannot be used on type 'CustomView'
button.addTarget(self, action: #selector(touchUpInside), for: .touchUpInside)
}
#objc func touchUpInside(_ sender: UIButton) {
print("Button clicked")
}
}
I've uploaded the source code to github. Here's the link https://github.com/felixmariaa/AwakeFromNibTest/
This is supposed to work and I'm not sure what is going wrong.
When typing awakeFromNib, I used the autocomplete provided by XCode to complete the function, which resulted in the below code:
override class func awakeFromNib() {
}
Notice the class in the func declaration. This was causing the error:
I removed it and the code worked fine. Thought this would help someone.

Is it a good way to pass data to custom view then execute the function?

I created a custom input accessory view, it is the submit button.
However, I need to pass the data to the custom view then execute the further function. It is a good way to do that?
class SignUpViewController: UIViewController {
#IBOutlet weak var phoneTF: SignLogTextField!
#IBOutlet weak var EmailTF: SignLogTextField!
#IBOutlet weak var PasswordTF: SignLogTextField!
#IBOutlet weak var FBBtn: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
textFieldPreparation()
}
func textFieldPreparation(){
EmailTF.inputAccessoryView = Bundle.main.loadNibNamed("SignSubmitBTN", owner: self, options: nil)?.first as! SignSubmitBTN
phoneTF.inputAccessoryView = Bundle.main.loadNibNamed("SignSubmitBTN", owner: self, options: nil)?.first as! SignSubmitBTN
PasswordTF.inputAccessoryView = Bundle.main.loadNibNamed("SignSubmitBTN", owner: self, options: nil)?.first as! SignSubmitBTN
}
}
I am not sure how to pass the data to the custom view or should I do the sign up in the Outlet Action?
It is my custom view
import UIKit
class SignSubmitBTN: UIView {
#IBAction func submitAction(_ sender: Any) {
}
#IBOutlet weak var subBTN: UIButton!
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
func setup(){}
}
If I have to pass data to custom view should I use protocol? If I should use the protocol of how to use it?
OK...
I think you are approaching this from the wrong direction. The responsibility of a button should be to tell you that a user has tapped it and nothing more. The button should not be dealing with signing in.
But... you are 90% of the way there here. Just a few more bits to add.
You can update your submit button to include a delegate and use the delegate in your button action...
import UIKit
// protocol
protocol SignInButtonDelegate: class {
func signIn()
}
class SignSubmitBTN: UIView {
// property for delegate
weak var delegate: SignInButtonDelegate?
#IBAction func submitAction(_ sender: Any) {
// this tells the delegate to sign in
// it doesn't need to know how that happens
delegate?.signIn()
}
#IBOutlet weak var subBTN: UIButton!
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
func setup() {}
}
Then in your view controller you conform to the delegate protocol...
extension SignUpViewController: SignInButtonDelegate {
func signIn() {
// here you already have access to all the data you need to sign in.
// you are in the view controller here so just get the text from the username, password, etc...
}
}
And then set the view controller as the delegate...
func textFieldPreparation() {
let signInButton = Bundle.main.loadNibNamed("SignSubmitBTN", owner: self, options: nil)?.first as! SignSubmitBTN
signInButton.delegate = self
// these are properties... they should begin with a lowercase letter
emailTF.inputAccessoryView = signInButton
phoneTF.inputAccessoryView = signInButton
passwordTF.inputAccessoryView = signInButton
}
Your CustomView is just a class at the end, so you can do it in object oriented paratime, For that write a function in your customView to pass data in it. Like
class SignSubmitBTN: UIView {
var data: String!;
public func setData(data: String) {
self.data = data;
}
/// Other code
}
And to set data after initializing your CustomView, call setData(params) function to set data in it.
Try this
func loadFromNib() -> SignSubmitBTN {
let bundle = Bundle.main.loadNibNamed("SignSubmitBTN", owner: self, options: nil)?.first as! SignSubmitBTN
return bundle
}
In your viewcontroller call like below:
let customObj = loadFromNib()
customObj.dataToGet = "Data to pass"
customObj.delegate = self
EmailTF.inputAccessoryView = customObj
If you want pass data from custom class, You need to use delegate protocol as #Fogmeister suggested.
If you want delegate option
public protocol menuOpen: class {
func openMenuAction(selectedValue : String)
}
class SignSubmitBTN: UIView {
open var delegate:menuOpen?
var dataToGet = ""
#IBAction func submitAction(_ sender: Any) {
self.delegate.openMenuAction("test")
}
}
Then add delegate method in your VC
class SignUpViewController: UIViewController,menuOpen{
func openMenuAction(selectedValue : String) {
//get your selected value here, you would better pass parameter in this method
}
}

how to access custom view button action in main VC

I have created a custom view xib and give that view class. Now I take a view in main vc and give that class but now I want to access custom view button action method in my main vc. So how can I do that?
Here is my custom view
import UIKit
class TextCustomisationVC: UIView {
#IBOutlet var contentView: UIView!
override init(frame: CGRect) {
super.init(frame: frame)
self.commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.commonInit()
}
private func commonInit(){
Bundle.main.loadNibNamed("TextCustomisationVC", owner: self, options: nil)
addSubview(contentView)
contentView.frame = self.bounds
contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
}
#IBAction func btnCloseCustomisation_Click(_ sender: Any) {
}
#IBAction func btnApplyCustomisation_Click(_ sender: Any) {
}
}
Now I create an outlet in my main VC and give that same class I can access those class outlets but now I want to access above button action method So how can I do that?
You can use delegate here which you can implement in the main VC.
create a protocol like this:
protocol ButtonActionDelegate {
func closeButtonPressed(_ sender:UIButton)
func applyButtonPressed(_ sender:UIButton)
}
Then create instance of the delegate in your view like this:
var delegate:ButtonActionDelegate?
Implement this delegate in the main VC like this:
extension mainVC : ButtonActionDelegate {
func closeButtonPressed(_ sender: UIButton) {
}
func applyButtonPressed(_ sender: UIButton) {
}
}
Then you can call the delegate methods respectively like this:
#IBAction func btnCloseCustomisation_Click(_ sender: Any) {
self.delegate?.closeButtonPressed(sender)
}
#IBAction func btnApplyCustomisation_Click(_ sender: Any) {
self.delegate?.applyButtonPressed(sender)
}
You can try
let cusView = TextCustomisationVC(frame:///)
if btn sender is used inside function
cusView.btnCloseCustomisation_Click(cusView.closeBtn)
otherwise send any dummy button
cusView.btnCloseCustomisation_Click(UIButton())
Edit:
protocol CustomTeller {
func closeClicked(UIButton)
}
class TextCustomisationVC: UIView {
var delegate: CustomTeller?
#IBAction func btnCloseCustomisation_Click(_ sender: UIButton) {
self.delegate?.closeClicked(sender:sender)
}
}
// in mainVC
let cusView = TextCustomisationVC(frame:///)
cusView.delegate = self
and implement
func closeClicked(sender:UIButton) {
// close button called
}

Resources