Understanding UIViewRepresentable - swift

Swift 5.0 iOS 13
Trying to understand how UIViewRepresentable works, and put together this simple example, almost there, but maybe its complete nonsense. Yes, I know there is already a tapGesture in SwiftUI, this is just a test.
Won't compile cause it says 'super.init' isn't called on all paths before returning from initialiser, which I try and set but obviously not correctly.
import SwiftUI
struct newView: UIViewRepresentable {
typealias UIViewType = UIView
var v = UIView()
func updateUIView(_ uiView: UIView, context: Context) {
v.backgroundColor = UIColor.yellow
}
func makeUIView(context: Context) -> UIView {
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(Coordinator.handleTap(sender:)))
v.addGestureRecognizer(tapGesture)
return v
}
func makeCoordinator() -> newView.Coordinator {
Coordinator(v)
}
final class Coordinator: UIView {
private let view: UIView
init(_ view: UIView) {
self.view = view
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
#objc func handleTap(sender: UITapGestureRecognizer) {
print("tap")
}
}
}

Just make your Coordinator is a NSObject, it usually plays bridge/controller/delegate/actor role, but not presentation, so should not be is-a-UIView
final class Coordinator: NSObject {
private let view: UIView
init(_ view: UIView) {
self.view = view
}
and one more...
func makeUIView(context: Context) -> UIView {
// make target a coordinator, which is already present in context !!
let tapGesture = UITapGestureRecognizer(target: context.coordinator,
action: #selector(Coordinator.handleTap(sender:)))
v.addGestureRecognizer(tapGesture)
return v
}

Thats because your Coordinator is a subclass of the UIView and you
Must call a designated initializer of the superclass 'UIView'
before returning from the init:
init(_ view: UIView) {
self.view = view
super.init(frame: .zero) // Or any other frame you need
}

Related

Swift UITapGestureRecognizer not calling

Pretty simple problem that is making out to be harder to solve than it should: My gesture is simple not calling, at all. I am using a uiviewrepresentable that is displayed inside of a zstack. If i add a .tapgesture{} to CameraView() directly it works just fine. But i need to get the tap position
public struct CameraView: UIViewRepresentable {
#EnvironmentObject var ue: UserEvents
public func makeUIView(context: Context) -> UIView {
let view = UIView(frame: UIScreen.main.bounds)
let focusGesture = UITapGestureRecognizer(target: self, action: #selector(context.coordinator.tapFocus(_:)))
self.ue.cameraPreview = AVCaptureVideoPreviewLayer(session: ue.session)
self.ue.cameraPreview.frame = view.frame
self.ue.cameraPreview.videoGravity = ue.videoGravity
self.ue.session.startRunning()
view.isUserInteractionEnabled = true
view.layer.addSublayer(self.ue.cameraPreview)
focusGesture.numberOfTapsRequired = 1
view.addGestureRecognizer(focusGesture)
return view
}
public func updateUIView(_ uiView: UIViewType, context: Context) { }
public func makeCoordinator() -> Self.Coordinator {
return Coordinator()
}
public class Coordinator: NSObject {
#objc public func tapFocus(_ sender: UITapGestureRecognizer) {
print("tap")
}
}
}
The target should be the coordinator (which is a persistent entity, unlike the transient View), not self.
let focusGesture = UITapGestureRecognizer(target: context.coordinator, action: #selector(context.coordinator.tapFocus(_:)))

Call to IBOutlet in custom UIView from another class

I am looking for a way to refer to the IBOutlet variable in a custom class UIView, from another class. I found layoutSubviews, but each change only works on the first call, and not on each subsequent call. Thanks for help!
ViewController class:
var SB = StatusBar()
SB.update(1)
SB.update(2)
SB.update(3)
StatusBar class:
class StatusBar: UIView {
#IBOutlet var view: UIView!
#IBOutlet weak var label: UILabel!
var ActualStatus: Int!
required init?(coder: NSCoder) {
super.init(coder: coder)
xibSetup()
}
override init(frame: CGRect) {
super.init(frame: frame)
xibSetup()
}
func update(status) {
ActualStatus = status
self.layoutSubviews()
}
override func layoutSubviews() {
super.layoutSubviews()
label.text = ActualStatus
}
func xibSetup() {
view = loadViewFromNib()
view.frame = bounds
view.autoresizingMask = [UIView.AutoresizingMask.flexibleWidth, UIView.AutoresizingMask.flexibleHeight]
addSubview(view)
}
func loadViewFromNib() -> UIView {
let bundle = Bundle(for: type(of:self))
let nib = UINib(nibName: "StatusBar", bundle: bundle)
let view = nib.instantiate(withOwner: self, options: nil)[0] as! UIView
return view
}
}
Result: label.text is 1, change only works on the first call

Passing data from UIViewController to Custom UIView with XIB

I have the following swift file which controls a xib file
import UIKit
protocol SelectProfile: class {
func selectionUp(id: Int, selected: Bool)
}
class Component: UIView {
#IBOutlet weak var name: UILabel!
#IBOutlet weak var selection: UIView!
var selected: Bool = false
var profileId: Int = 6
weak var delegate: SelectProfile?
#IBAction func selProfile(_ sender: Any) {
selected = !selected
selection.isHidden = !selected
delegate?.selectionUp(id: profileId, selected: selected)
}
let nibName = "Component"
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
func commonInit() {
guard let view = loadViewFromNib() else { return }
view.frame = self.bounds
self.addSubview(view)
selection.isHidden = true
name.text = String(profileId)
}
func loadViewFromNib() -> UIView? {
let nib = UINib(nibName: nibName, bundle: nil)
return nib.instantiate(withOwner: self, options: nil).first as? UIView
}
}
In my UIViewController where I want to interact with the component I have the following code (shortened for brevity)
import UIKit
class Home: UIViewController, SelectProfile {
#IBOutlet weak var profileComponent: Component!
func selectionUp(id: Int, selected: Bool) {
print(id, selected)
}
override func viewDidLoad() {
profileComponent.delegate = self
profileComponent.profileId = 2
}
}
My delegate works nicely and I'm able to change the values for IBOutlets but I can't pass a value to variables. This line doesn't pass the data to my xib profileComponent.profileId = 2.
It seems like I have an issue with initialization of the xib but I don't know how to fix it.
The viewDidLoad of controller is called after view's init, so when you change profileId as you do, it is changed, but not reflected anywhere.
I assume the following, at least, missed:
var profileId: Int = 6 {
didSet {
name.text = String(profileId)
// call delegate here if needed as well
}
}

Hide Custom View UIButton From UIViewController Class

Actually i have a Custom view with two button, and i want to hide it at runtime through UIViewController , So i don't get any exact thing to hide that button from UIViewcontroller class
Here is my CustomView class,
import UIKit
class BottomButtonUIView: UIView {
#IBOutlet weak var btnNewOrder: UIButton!
#IBOutlet weak var btnChat: UIButton!
override func awakeFromNib() {
super.awakeFromNib()
}
// MARK: init
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
if self.subviews.count == 0 {
setup()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
func setup() {
if let view = Bundle.main.loadNibNamed("BottomButtonUIView", owner: self, options: nil)?.first as? BottomButtonUIView {
view.frame = bounds
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
addSubview(view)
}
}
#IBAction func btnOrderNowClick(_ sender: Any) {
let VC1 = StoryBoardModel.orderDeatalStoryBord.instantiateViewController(withIdentifier: "NewOrderViewController") as! NewOrderViewController
VC1.isPush = false
let navController = UINavigationController(rootViewController: VC1) // Creating a navigation controller with VC1 at the root of the navigation stack.
let currentController = getCurrentVC.getCurrentViewController()
currentController?.present(navController, animated:true, completion: nil)
}
#IBAction func btnChatNowClick(_ sender: Any) {
}
func getCurrentViewController() -> UIViewController? {
if let rootController = UIApplication.shared.keyWindow?.rootViewController {
var currentController: UIViewController! = rootController
while( currentController.presentedViewController != nil ) {
currentController = currentController.presentedViewController
}
return currentController
}
return nil
}
}
I set it to UIView in StoryBoard, and then I create outlet of that view,
#IBOutlet weak var viewBottmNewOrder: BottomButtonUIView!
Now i want to hide btnNewOrder from UIViewcontroller class but when i use
viewBottmNewOrder.btnNewOrder.isHidden = true it cause null exception, Please do need full answer.
Please don't do like that. The required init(coder aDecoder: NSCoder) will call a lot of times when the BottomButtonUIView created from xib. And your custom view will look like:
[BottomButtonUIView [ BottomButtonUIView [btnNewOrder, btnChat]]].
So when you access to btnNewOrder like that:
viewBottmNewOrder.btnNewOrder it will null.
I think you should add your custom view in viewDidLoad of your `UIViewController'.

Start animation once a UIView appeared on screen

I have a custom UIView that I want to cover the screen once the user taps a button. It kind of simulates a custom view. There is child UIView in the custom UIView that should animate from the bottom (hidden at first) up to it's normal position (visible at the bottom). The problem that I am having is that it seems like layoutSubviews is a bad place to start doing animations. Where would the correct place be? Something like viewDidAppear but for UIViews.
In UIViewController:
let rect: CGRect = CGRectMake(0, 0, view.bounds.size.width, view.bounds.size.height)
let alertView = AlertView(frame: rect)
view.addSubview(alertView)
In the AlertView:
import UIKit
class AlertView: UIView {
let nibName = "AlertView"
let animationDuration = 0.5
var view: UIView!
#IBOutlet weak var notificationView: UIView!
#IBOutlet weak var notificationBottomConstraint: NSLayoutConstraint!
override init(frame: CGRect) {
super.init(frame: frame)
viewSetup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
viewSetup()
}
override func didAddSubview(subview: UIView) {
super.didAddSubview(subview)
}
func viewSetup() {
view = loadViewFromNib()
view.frame = bounds
view.autoresizingMask = [UIViewAutoresizing.FlexibleWidth, UIViewAutoresizing.FlexibleHeight]
// move the notification view offscreen
notificationBottomConstraint.constant = -notificationView.frame.size.height
addSubview(view)
}
func loadViewFromNib() -> UIView {
let bundle = NSBundle(forClass: self.dynamicType)
let nib = UINib(nibName: nibName, bundle: bundle)
return nib.instantiateWithOwner(self, options: nil)[0] as! UIView
}
override func layoutSubviews() {
super.layoutSubviews()
print("layoutSubviews")
animate()
}
func animate() {
// move the notification up
self.notificationBottomConstraint.constant = 0
UIView.animateWithDuration(animationDuration) { () -> Void in
self.view.setNeedsDisplay()
}
}
}
I suggest you to call your animate() function from one of these methods:
willMoveToSuperview:
didMoveToSuperview
These methods of UIView as needed to track the movement of the current view in your view hierarchy.
Reference: UIView class
Your final constraint value is not in your animate closure. Try this:
func animate() {
// move the notification up
UIView.animateWithDuration(animationDuration) { () -> Void in
self.notificationBottomConstraint.constant = 0
self.view.setNeedsDisplay()
}
}