I have 3 UIViewControllers. ContainerVC which contains 2 ContainerViews. First Container View is DashboardVC and second one is SidebarVC. The DashboardVC covers the entire screen, while the SidebarVC is outside.
I have a leading constraint for the SidebarVC that should be animated and the SidebarVC should slide in (from the left side). On the DashboardVC I have a UIBarButtonItem and when it's pressed it should perform the animation. The problem is that I'm doing something wrong with the delegate and when the ContainerVC conforms to the protocol, nothing happens.
PS: I have very hard time understanding protocols/delegates despite having watch a bunch of different videos on this concept. Here's the code:
DashboardVC
protocol SideBarDelegate {
func showMenu()
func hideMenu()
}
class DashboardVC: UIViewController {
var delegate: SideBarDelegate?
var isSideMenuOpen = true
#IBAction func menuButtonPressed(_ sender: UIBarButtonItem) {
if isSideMenuOpen {
delegate?.showMenu()
isSideMenuOpen = false
}
else {
delegate?.hideMenu()
isSideMenuOpen = true
}
}
}
ContainerVC
class ContainerVC: UIViewController {
#IBOutlet weak var sideBarMenuLeadingConstraint: NSLayoutConstraint!
}
extension ContainerVC : SideBarDelegate {
func showMenu() {
sideBarMenuLeadingConstraint.constant = -290
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}
}
func hideMenu() {
sideBarMenuLeadingConstraint.constant = 0
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}
}
}
You use the delegate only on classes. To prevent memory leaks, do those two things:
Change:
protocol SideBarDelegate {
func showMenu()
func hideMenu()
}
to:
protocol SideBarDelegate: class {
func showMenu()
func hideMenu()
}
Now, rename delegate property to:
weak var delegate: SideBarDelegate?
Weak does not increase the reference counting. This is important to prevent memory leaks.
Your instance of ContainerVC must have some sort of reference to an instance of DashboardVC (or make the delegate static but I have never seen something like that). Then, in your viewDidLoad method of ContainterVC, set this:
myInstanceReferenceToDashboardVC.delegate = self
Related
I have a UIbutton class - from which I would like to call an 'adjust constraints' method after the user makes changes on the setting screen. I have created some protocols and all seems in order but it is not calling the method from the subview UIButton after the user closes the Setting Screen.
I have tried some of the other solutions here - that hasn't worked and I think it might be because I am using a UIButton class and I can't reinstantiate it, or call the instantiation? Either way, it never calls the method from the delegate.
Is using protocols the right way to solve this problem and if so, what am I missing?
Basically I have 3 files; the MainVC which I set as my first delegate (it gets triggered from my SettingScreenVC when user is done making changes to Setting Screen):
class MainVC: UIViewController, SettingScreenDelegate {
weak var numButtonDelegate: Buttons_Numeric?
func settingSetButtonConstraints() {
numButtonDelegate?.setupButtonConstraints()
}
}
Then in my Setting Screen I call the MainVC after the user made some changes to their settings:
class MainVC: SettingScreenVC {
weak var delegate: SettingScreenDelegate?
func closeSettings() {
delegate?.settingSetButtonConstraints()
}
}
Then in my Buttons_Numeric class I declare the function and the UIButton class delegate:
protocol numButtonDelegate: class {
func setupButtonConstraints()
}
class Buttons_Numeric: UIButton, numButtonDelegate {
weak var numButtonDelegate: Buttons_Numeric?
required init(coder aDecoder:NSCoder) { super.init(coder: aDecoder)!}
override init(frame:CGRect) {super.init(frame: frame)
self.numButtonDelegate = self
setupButtonConstraints()
}
override func awakeFromNib() {
self.numButtonDelegate = self
setupButtonConstraints()
}
func setupButtonConstraints() {
//SET UP CONSTRAINTS
}
}
Ok so couple of things you need to understand about delegates:
if you create a delegate, you need to conform to the delegate somewhere.
the conforming class should be assigned to the instance of the delegate.
With that in mind lets try to fix the code:
first the settings screen:
protocol SettingScreenDelegate: class {
func settingSetButtonConstraints()
}
class SettingScreenVC {
weak var delegate: SettingScreenDelegate?
func closeSettings() {
delegate?.settingSetButtonConstraints()
}
}
So far so good now the mainScreen should conform to the SettingScreenDelegate and be assigned to its delegate:
class MainVC: UIViewController, SettingScreenDelegate {
weak var button: Buttons_Numeric!
func openSettingsScreen() {
let settingsScreen = ... // the setting screen instanciation
settingsScreen.delegate = self // the MainVC
}
func settingSetButtonConstraints() {
self.button.setupButtonConstraints()
}
}
Now for the last step, the MainVC should have an instance of the button, then the only thing we need to do is call the function 'setupButtonConstraints' from the MainVC which means we do not need the delegate at the button.
class Buttons_Numeric: UIButton {
required init(coder aDecoder:NSCoder) { super.init(coder: aDecoder)!}
override init(frame:CGRect) {super.init(frame: frame)
setupButtonConstraints()
}
override func awakeFromNib() {
setupButtonConstraints()
}
func setupButtonConstraints() {
//SET UP CONSTRAINTS
}
}
You need to assign a value to the delegate variable. I would do it like this if the answer is always self
class Example: SomeDelegate {
lazy var someDelegate: SomeDelegate? = self
}
Otherwise you'll want to do it in the class' initializer or ViewDidLoad.
I wanted to implement my own HUD for a UIViewCntroller and a UIView, so I did this:
protocol ViewHudProtocol {
func showLoadingView()
func hideLoadingView()
}
extension ViewHudProtocol where Self: UIView {
func showLoadingView() { //Show HUD by adding a custom UIView to self.}
}
func hideLoadingView() {
}
}
Now I can easily adopt ViewHudProtocol on any UIView to call showLoadingView and hideLoadingView. The problem is I want to use the same protocol for UIViewController, so I did this:
extension ViewHudProtocol where Self: UIViewController {
func showLoadingView() {
self.view.showLoadingView() //Error: UIView has no member showLoadingView
}
func hideLoadingView() {
self.view.hideLoadingView() //Error: UIView has no member hideLoadingView
}
}
I agree to the error that UIView has not adopted the protocol yet. So I did this:
extension UIView: ViewHudProtocol {}
And it works. Is there a better way to do this? I mean it feels wrong to extend every view with ViewHudProtocol, where not all of them will use it. If I could do something like, "only adopt ViewHudProtocol implicitly for a UIView, if its UIViewController demands for it. Else you could adopt ViewHUDProtocol manually on any UIView when required."
I would solve this with the following approach, using associatedtype, defined only for needed views and/or controllers (tested in Xcode 11.2 / swift 5.1):
protocol ViewHudProtocol {
associatedtype Content : ViewHudProtocol
var content: Self.Content { get }
func showLoadingView()
func hideLoadingView()
}
extension ViewHudProtocol where Self: UIView {
var content: some ViewHudProtocol {
return self
}
func showLoadingView() { //Show HUD by adding a custom UIView to self.}
}
func hideLoadingView() {
}
}
extension ViewHudProtocol where Self: UIViewController {
func showLoadingView() {
self.content.showLoadingView() //NO Error
}
func hideLoadingView() {
self.content.hideLoadingView() //NO Error
}
}
//Usage
extension UITableView: ViewHudProtocol { // only for specific view
}
extension UITableViewController: ViewHudProtocol { // only for specific controller
var content: some ViewHudProtocol {
return self.tableView
}
}
The problem
So you want to constraint the conformance of a UIViewController to the protocol ViewHudProtocol only when the UIViewController.view property conforms to ViewHudProtocol.
I am afraid this is not possible.
Understanding the problem
Let's have a better look at your problem
You have 2 types (UIView and UIViewController) and you want to add to both the same functionalities
func showLoadingView()
func hideLoadingView()
What Mick West teaches us
This kind of scenario is somehow similar to what Mick West faced during the development of the Tony Hawks series Mick West and an elegant solution is described in its article Evolve your hierarchy.
Solution
We can apply that approach to your problem and here's the solution
struct HudViewComponent {
let view: UIView
private let hud: UIView
init(view: UIView) {
self.view = view
self.hud = UIView(frame: view.frame)
self.hud.isHidden = true
self.view.addSubview(hud)
}
func showLoadingView() {
self.hud.isHidden = false
}
func hideLoadingView() {
self.hud.isHidden = true
}
}
protocol HasHudViewComponent {
var hidViewComponent: HudViewComponent { get }
}
extension HasHudViewComponent {
func showLoadingView() {
hidViewComponent.showLoadingView()
}
func hideLoadingView() {
hidViewComponent.hideLoadingView()
}
}
That's it, now you can add the hud functionalities to any Type conforming to HasHudViewComponent.
class SomeView: UIView, HasHudViewComponent {
lazy var hidViewComponent: HudViewComponent = { return HudViewComponent(view: self) }()
}
or
class MyViewController: UIViewController, HasHudViewComponent {
lazy var hidViewComponent: HudViewComponent = { return HudViewComponent(view: self.view) }()
}
Considerations
As you can see the idea is to thinking in terms of components.
You build a component (HudViewComponent) with your hud functionalities. The component only asks for the minimum requirements: it needs a UIView.
Next you define the HasHudViewComponent which states that the current type has a HudViewComponent property.
Finally you can add your hud functionalities to any Type which has a view (UIView, UIViewController, ...) simply conforming your type to HasHudViewComponent.
Notes
You asked an interesting question and I know this does not answers 100% what you were looking for, but by a practical point of view it should provides you with a tool to achieve what you need.
I would have taken this approach:
Create a UIView Class,
setup the view
Declare a shared object.
A function to show the view
A function to remove the view. and then call it in view controllers as IndicatorView.shared.show() , IndicatorView.shared.hide()
import Foundation
import UIKit
import Lottie
class IndicatorView : UIView {
static let shared = IndicatorView()
var loadingAnimation : AnimationView = {
let lottieView = AnimationView()
lottieView.translatesAutoresizingMaskIntoConstraints = false
lottieView.layer.masksToBounds = true
return lottieView
}()
var loadingLabel : UILabel = {
let label = UILabel()
label.textColor = .white
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont(name: "SegoeUI", size: 12)
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
translatesAutoresizingMaskIntoConstraints = false
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func show() {
setupLoadingView()
self.alpha = 0
UIView.animate(withDuration: 0.5, animations: {
self.isHidden = false
self.alpha = 1
}, completion: nil)
applyLottieAnimation()
}
public func hide() {
self.alpha = 1
UIView.animate(withDuration: 0.5, animations: {
self.alpha = 0
}, completion: { _ in
self.isHidden = true
self.removeFromSuperview()
}) }
private func setupLoadingView() {
let controller = UIApplication.shared.keyWindow!.rootViewController!
controller.view.addSubview(self)
//setup your views here
self.setNeedsLayout()
self.reloadInputViews()
}
}
For this particular scenario, a Decorator would work better, and result in a better design:
final class HUDDecorator {
private let view: UIView
init(_ view: UIView) {
self.view = view
}
func showLoadingView() {
// add the spinner
}
func hideLoadingView() {
// remove the spinner
}
}
Using the Decorator would then be as easy as declaring a property for it:
class MyViewController: UIViewController {
lazy var hudDecorator = HUDDecorator(view)
}
This will allow any controller to decide if it wants support for showing/hiding a loading view by simply exposing this property.
Protocols are too invasive for simple tasks like enhancing the looks on a UI component, and they have the disadvantage of forcing all views of a certain class to expose the protocol functionality, while the Decorator approach allows you to decide which view instances to receive the functionality.
I am working on two views that are subclassing subclass of UITableViewCell. In the base one (subclass of UITableViewCell) I am trying to setup gesture recognizer in a way that each of super class could change the behavior (eventually call didTapped method on it's delegate) of the tap.
I have written following code. I can use #selector(tap), however I think that using a variable instead of overriding a tap method in each super class is a much cleaner way. Is it even possible to use something like #selector(tapFunc)? If no what would be the cleanest and best from engineering point of view solution?
class BaseCell: UITableViewCell {
#objc var tapFunc: () -> () = { () in
print("Tapped")
}
#objc func tap() {
print("TEST")
}
func setupBasicViews(withContent: () -> ()) {
let tapGestureRecoginzer = UITapGestureRecognizer(target: self, action: #selector(tapFunc))
contentView.isUserInteractionEnabled = true
contentView.addGestureRecognizer(tapGestureRecoginzer)
}
}
And then two views that are building on top of this one:
class ViewA: BaseCell {
//don't want to do this
override func tap() {
//do stuff
}
func setup {
//setup everything else
}
class ViewB: BaseCell {
var delegate: ViewBProtocool?
func setup {
tapFunc = { () in
delegate?.didTapped(self)
}
//setup everything else
}
You're not too far off. Make the following changes:
class BaseCell: UITableViewCell {
var tapFunc: (() -> Void)? = nil
// Called by tap gesture
#objc func tap() {
tapFunc?()
}
func setupBasicViews(withContent: () -> ()) {
let tapGestureRecoginzer = UITapGestureRecognizer(target: self, action: #selector(tap))
contentView.isUserInteractionEnabled = true
contentView.addGestureRecognizer(tapGestureRecoginzer)
}
}
class ViewA: BaseCell {
func setup() {
//setup everything else
}
}
class ViewB: BaseCell {
var delegate: ViewBProtocol?
func setup() {
tapFunc = {
delegate?.didTapped(self)
}
//setup everything else
}
}
Now each subclass can optionally provide a closure for the tapFunc property.
I show above that tapFunc is optional with no default functionality in the base class. Feel free to change that to provide some default functionality if desired.
I'm working through a tutorial that uses NSSpeechSynthesizer and two of its NSSpeechSynthesizerDelegate protocol methods. In my ViewController, I don't explicitly call the protocol methods so I'm curious as to what do I need to research in order to understand how these methods are called during runtime? The delegate methods are working as expected but I'm wondering how are they being called which makes this possible?
import Cocoa
class MainWindowController: NSWindowController, NSSpeechSynthesizerDelegate, NSWindowDelegate {
//Now MainWindowController is more powerful by having its own KITT being able to delegate powerful functionality and do less work. The delegate will do all the heavy lifting and return the results to MainWindowController instances.
// MARK: - Properties
#IBOutlet weak var textField: NSTextField!
#IBOutlet weak var speakButton: NSButton!
#IBOutlet weak var stopButton: NSButton!
let speechSynth = NSSpeechSynthesizer.init(voice: NSSpeechSynthesizer.VoiceName.init(rawValue: "com.apple.speech.synthesis.voice.Victoria"))
var isSpeaking: Bool = false {
didSet {
updateButtons()
}
}
// MARK: - Overriden Properties
override var windowNibName: NSNib.Name? {
return NSNib.Name("MainWindowController")
}
// MARK: - Overidden Methods
override func windowDidLoad() {
super.windowDidLoad()
updateButtons()
speechSynth?.delegate = self
}
// MARK: - UI methods
#IBAction func speakIt(sender: NSButton) {
//Get tuype-in text as a string
let string = textField.stringValue
if string.isEmpty {
print("string from \(textField) is empty")
} else {
speechSynth?.startSpeaking(string)
isSpeaking = true
}
}
#IBAction func stopIt(sender: NSButton) {
speechSynth?.stopSpeaking()
}
func updateButtons(){
if isSpeaking {
speakButton.isEnabled = false
stopButton.isEnabled = true
} else {
speakButton.isEnabled = true
stopButton.isEnabled = false
}
}
// MARK: - NSSpeechSynthesizerDelegate Methods
//this functionality is considered more powerful and is made possible due to the speechSynthesizer.delegate = self
//the delegate is doing the work and reporting that completed work to the MainWindowController instance
//so kinda like the delegate is providing the signature and its up to us as the developers based on what we do with those parameters inside the function in order for us to add our own creativity.
func speechSynthesizer(_ sender: NSSpeechSynthesizer, didFinishSpeaking finishedSpeaking: Bool) {
//by setting this variable to FALSE, it will fire off the didSet computed property which this variable has both storage and behavior.
isSpeaking = false
}
// MARK: - NSWindowDelegate Methods
func windowShouldClose(_ sender: NSWindow) -> Bool {
return !isSpeaking
}
}
Your windowDidLoad method contains this line:
speechSynth?.delegate = self
This means the speech synthesizer object has a reference back to your MainWindowController, so the speech synthesizer object can send messages to your MainWindowController.
A simplified implementation inside NSSpeechSynthesizer could look something like this in Swift:
class NSSpeechSynthesizer: NSSoundDelegate {
weak var delegate: NSSpeechSynthesizerDelegate?
func startSpeaking(_ string: String) {
guard
let audioData = audioData(for: string),
let sound = NSSound(data: audioData)
else { return }
sound.delegate = self
sound.play()
}
// Part of NSSoundDelegate
func sound(_ sound: NSSound, didFinishPlaying finished: Bool) {
// The first ? means Swift only sends the message if
// delegate is not nil.
// The second ? means Swift only sends the message if delegate
// implements speechSynthesizer(_:didFinishSpeaking:).
delegate?.speechSynthesizer?(self, didFinishSpeaking: finished)
}
}
But it's actually implemented in Objective-C, where you have to be more verbose about checking whether the delegate handles the message:
- (void)sound:(NSSound *)sound didFinishPlaying:(BOOL)finished {
if ([delegate respondsToSelector:#selector(speechSynthesizer:didFinishSpeaking:)]) {
[delegate speechSynthesizer:self didFinishSpeaking:finished];
}
}
By using the delegation protocol I have tried to pass a string (inputFromUser.string) from NSViewController - mainController to custom subclass of NSView of NSPopover - PlasmidMapView, to drawRect function, see code below. But, it didn’t work. I don’t know where a mistake is. Maybe there is another way to pass this string.
Update
File 1.
protocol PlasmidMapDelegate {
func giveDataForPLasmidMap(dna: String)
}
class MainController: NSViewController {
#IBOutlet var inputFromUser: NSTextView!
var delegate: plasmidMapDelegate?
#IBAction func actionPopoverPlasmidMap(sender: AnyObject) {
popoverPlasmidMap.showRelativeToRect(sender.bounds,
ofView: sender as! NSView, preferredEdge: NSRectEdge.MinY)
let dna = inputDnaFromUser.string
delegate?.giveDataForPLasmidMap(dna!)
}
}
File 2
class PlasmidMapView: NSView, PlasmidMapDelegate {
var dnaForMap = String()
func giveDataForPLasmidMap(dna: String) {
dnaForMap = dna
}
override func drawRect(dirtyRect: NSRect) {
let objectOfMainController = MainController()
objectOfMainController.delegate = self
//here I have checked if the string dnaForMap is passed
let lengthOfString = CGFloat(dnaForMap.characters.count / 10)
let pathRect = NSInsetRect(self.bounds, 10, 45)
let path = NSBezierPath(roundedRect: pathRect,
xRadius: 5, yRadius: 5)
path.lineWidth = lengthOfString //the thickness of the line should vary in dependence on the number of typed letter in the NSTextView window - inputDnaFromUser
NSColor.lightGrayColor().setStroke()
path.stroke()
}
}
Ok, there's some architecture mistakes. You don't need delegate method and protocol at all. All you just need is well defined setter method:
I. Place your PlasmidMapView into NSViewController-subclass. This view controller must be set as contentViewController-property of your NSPopover-control. Don't forget to set it the way you need in viewDidLoad-method or another.
class PlasmidMapController : NSViewController {
weak var mapView: PlacmidMapView!
}
II. In your PlacmidMapView don't forget to call needsDisplay-method on dna did set:
class PlasmidMapView: NSView {
//...
var dnaForMap = String() {
didSet {
needsDisplay()
}
//...
}
III. Set dna-string whenever you need from your MainController-class.
#IBAction func actionPopoverPlasmidMap(sender: AnyObject) {
popoverPlasmidMap.showRelativeToRect(sender.bounds,
ofView: sender as! NSView, preferredEdge: NSRectEdge.MinY)
let dna = inputDnaFromUser.string
if let controller = popoverPlasmidMap.contentViewController as? PlasmidMapController {
controller.mapView.dna = dna
} else {
fatalError("Invalid popover content view controller")
}
}
In order to use delegation your class PlasmidMapView needs to have an instance of the MainController (btw name convention is Class, not class) and conform to the PlasmidMapDelegate (once again name convention dictates that it should be PlasmidMapDelegate). With that instance you then can:
mainController.delegate = self
So, after several days I have found a solution without any protocols and delegation as Astoria has mentioned. All what I needed to do was to make #IBOutlet var plasmidMapIBOutlet: PlasmidMapView!for my custom NSView in MainController class and then to use it to set the value for the dnaForMap in #IBAction func actionPopoverPlasmidMap(sender: AnyObject).
class PlasmidMapView: NSView
{
var dnaForMap = String()
}
class MainController: NSViewController
{
#IBOutlet var inputFromUser: NSTextView!
#IBOutlet var plasmidMapIBOutlet: PlasmidMapView!
#IBAction func actionPopoverPlasmidMap(sender: AnyObject)
{
plasmidMapIBOutlet.dnaForMap = inputDnaFromUser.string!
popoverPlasmidMap.showRelativeToRect(sender.bounds,
ofView: sender as! NSView, preferredEdge: NSRectEdge.MinY)
}
}