I have this function in a helper class. It's to show/hide some stuff.
func showHideDetails(controller: UIViewController, isHidden: Bool) {
...
if controller is AddNewViewController {
let addNewViewController = controller as! AddNewViewController
addNewViewController.bgButton.isHidden = isHidden
} else if controller is EditViewController {
let editViewController = controller as! EditViewController
editViewController.bgButton.isHidden = isHidden
}
...
}
Is there is a way around to have one if statement, instead of one if statement for each controller? Something like,
if controller.hasProperty(bgButton) {
controller.bgButton.isHidden = isHidden
}
Thanks
You still need to cast using as? ..., however in order not to do that for all view controllers that have the bgButton, you can define a base protocol enforcing all classes conforming to it to have the bgButton:
public protocol Buttoned {
var bgButton: UIButton { get set }
func setHideButton(_ isHidden: Bool)
}
extension Buttoned {
public func setHideButton(_ isHidden: Bool) {
bgButton.isHidden = isHidden
}
}
public class AddNewViewController: Buttoned {
#IBOutlet fileprivate weak var bgButton: UIButton!
....
}
public class EditViewController: Buttoned {
#IBOutlet fileprivate weak var bgButton: UIButton!
....
}
then you can handle the action in the actual view controller like below:
func showHideDetails(controller: UIViewController, isHidden: Bool) {
...
if let controller = controller as? Buttoned {
controller.setHideButton(isHidden)
}
...
}
In the given scenario to replace your all if with single if means that you should have a common base class or your class conform to same protocol. However type cast still requires. You can use below code to achieve your desired functionality.
Create a protocol BackgroundButton
public protocol BackgroundButton {
var bgButton: UIButton { get }
}
Conforms all your custom UIViewController with this protocol in Extension like below
extension AddNewViewController : BackgroundButton {
var bgButton : UIButton {
return yourbutton // Use any instance of UIButton from your AddNewViewController
}
}
extension EditViewController : BackgroundButton {
var bgButton : UIButton {
return yourbutton // Use any instance of UIButton from your EditViewController
}
}
Finally update your method like this
func showHideDetails(controller: UIViewController, isHidden: Bool) {
...
if let controller = controller as? BackgroundButton {
controller.bgButton.isHidden = isHidden
controller.bgButton. //Do any thing which you want with your button
}
...
}
Hope this will help you to reduce your numbers of if
Related
why does typealias closure not transmit data and output nothing to the console? How to fix it?
class viewModel: NSObject {
var abc = ["123", "456", "789"]
typealias type = ([String]) -> Void
var send: type?
func createCharts(_ dataPoints: [String]) {
var dataEntry: [String] = []
for item in dataPoints {
dataEntry.append(item)
}
send?(dataEntry)
}
override init() {
super.init()
self.createCharts(abc)
}
}
class ViewController: UIViewController {
var viewModel: viewModel = viewModel()
func type() {
viewModel.send = { item in
print(item)
}
}
override func viewDidLoad() {
super.viewDidLoad()
print("hello")
type()
}
}
I have a project in which a similar design works, but I can not repeat it
The pattern is fine, but the timing is off.
You’re calling createCharts during the init of the view model. But the view controller is setting the send closure after the init of the view model is done.
Bottom line, you probably don’t want to call createCharts during the init of the view model.
Possible solution is to create custom initializer:
class viewModel: NSObject {
...
init(send: type?) {
self.send = send
self.createCharts(abc)
}
}
class ViewController: UIViewController {
var viewModel: viewModel = viewModel(send: { print($0) })
...
}
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.
In our app, we have a service that helps us decide which Modal UIVIewController should we present next. Every ModalVIewController has common function such as dismiss() but also a specific function it implements. So that's what we tried:
The base protocol that is common to all VC's base functions.
protocol ModalScreenDelegate: AnyObject {
func modalScreenWantsToDissmiss(_ modalScreen: ModalScreen)
}
A base protocol that every UIViewController implements
protocol ModalScreen: UIViewController {
var delegate: ModalScreenDelegate? { get set }
}
Now we create a protocol with specific-implementation of ModalScreenDelegate base protocol like so:
protocol ShareToFacebookDelegate: ModalScreenDelegate {
func someCustomMethod()
}
And assign it to:
class ShareToFacebookViewController: UIViewController, ModalScreen {
weak var delegate: ModalScreenDelegate? // **WORKS**
weak var delegate: ShareToFacebookDelegate? // **DOESN'T WORKS**
}
If I'm trying to use ShareToFacebookDelegate to instead of ModalScreenDelegate the compiler throws an IDE error saying I have to change it back to ModalScreenDelegate.
Why wouldn't it work? It's ShareToFacebookDelegate conforms to ModalScreenDelegate.
Any help would be highly appreciated.
Thank you!
UPDATE Based on Alexandr Kolesnik:
Your method works. But when I try to "fetch" the correct VC within the service under one method like so:
func fetchModal<T: ModalScreen & UIViewController>() -> T? {
return AddInstagramViewController.create() as? T
}
And then have a coordinator that wants to get this vc:
guard let currentModalViewController vc = modalScreenSupplierService.fetchModal() else {
return
}
I'm getting:
Generic parameter 'T' could not be inferred
And I can't really say what T will be, all I know that it's going to conform to UIViewController & ModalScreen. Is it solvable?
If I understood you correctly you can use generic types to manage the problem. Look through the code below. Hope it helps
protocol ModalScreenDelegate: AnyObject {
typealias T = ModalScreenDelegate
func modalScreenWantsToDissmiss(_ modalScreen: T)
}
protocol ShareToFacebookDelegate: ModalScreenDelegate {
func someCustomMethod()
}
protocol ModalScreen: UIViewController {
associatedtype T
var delegate: T? { get set }
}
class ShareToFacebookViewController: UIViewController, ModalScreen {
typealias T = ShareToFacebookDelegate
weak var delegate: T?
override func viewDidLoad() {
super.viewDidLoad()
delegate?.someCustomMethod()
}
}
UPDATE:
class AddInstagramViewController: SuperVC {
typealias T = ShareToFacebookDelegate
private var instaDelegate: ShareToFacebookDelegate?
override var delegate: ModalScreenDelegate? {
set {
instaDelegate = newValue as? ShareToFacebookDelegate
}
get {
return instaDelegate
}
}
static func create() -> AddInstagramViewController {
return AddInstagramViewController()
}
}
class SuperVC: UIViewController, ModalScreen {
typealias T = ModalScreenDelegate
var delegate: T?
}
class Supplier {
func fetchModal<M: ModalScreen>() -> M? { return AddInstagramViewController.create() as? M }
}
class SupplierImpl {
let modalScreenSupplierService: Supplier? = nil
func goto() {
guard
let vc: SuperVC = modalScreenSupplierService?.fetchModal()
else {
return
}
}
}
This solution:
protocol ModalScreenDelegate: AnyObject {
func modalScreenWantsToDissmiss(_ modalScreen: ModalScreen)
}
protocol ModalScreen: UIViewController {
var delegate: (ModalScreenDelegate & ShareToFacebookDelegate)? { get set }
}
protocol ShareToFacebookDelegate: ModalScreenDelegate {
func someCustomMethod()
}
class ShareToFacebookViewController: UIViewController, ModalScreen {
weak var delegate: (ModalScreenDelegate & ShareToFacebookDelegate)?
}
or inheritance:
protocol ModalScreenDelegate: AnyObject {
func modalScreenWantsToDissmiss(_ modalScreen: ModalScreen)
}
protocol ModalScreen: ShareToFacebookDelegate where Self: UIViewController {
var delegate: ModalScreenDelegate? { get set }
}
protocol ShareToFacebookDelegate: ModalScreenDelegate {
func someCustomMethod()
}
class ShareToFacebookViewController: UIViewController, ModalScreen {
func someCustomMethod() {
}
func modalScreenWantsToDissmiss(_ modalScreen: ModalScreen) {
}
weak var delegate: ModalScreenDelegate? // **WORKS**
}
I have a 13 lines func that is repeated in my app in every ViewController, which sums to a total of 690 lines of code across the entire project!
/// Adds Menu Button
func addMenuButton() {
let menuButton = UIButton(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
let menuImage = UIImage(named: "MenuWhite")
menuButton.setImage(menuImage, for: .normal)
menuButton.addTarget(self, action: #selector(menuTappedAction), for: .touchDown)
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: menuButton)
}
/// Launches the MenuViewController
#objc func menuTappedAction() {
coordinator?.openMenu()
}
for menuTappedAction function to work, I have to declare a weak var like this:
extension UIViewController {
weak var coordinator: MainCoordinator?
But by doing this I get error Extensions must not contain stored properties
What I tried so far:
1) Removing the weak keyword will cause conflicts in all my app.
2) Declaring this way:
weak var coordinator: MainCoordinator?
extension UIViewController {
Will silence the error but the coordinator will not perform any action. Any suggestion how to solve this problem?
You can move your addMenuButton() function to a protocol with a protocol extension. For example:
#objc protocol Coordinated: class {
var coordinator: MainCoordinator? { get set }
#objc func menuTappedAction()
}
extension Coordinated where Self: UIViewController {
func addMenuButton() {
let menuButton = UIButton(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
let menuImage = UIImage(named: "MenuWhite")
menuButton.setImage(menuImage, for: .normal)
menuButton.addTarget(self, action: #selector(menuTappedAction), for: .touchDown)
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: menuButton)
}
}
Unfortunately, you can't add #objc methods to class extensions (see: this stackoverflow question), so you'll still have to setup your view controllers like this:
class SomeViewController: UIViewController, Coordinated {
weak var coordinator: MainCoordinator?
/// Launches the MenuViewController
#objc func menuTappedAction() {
coordinator?.openMenu()
}
}
It'll save you some code, and it will allow you to refactor the bigger function addMenuButton(). Hope this helps!
For it to work in an extension you have to make it computed property like so : -
extension ViewController {
// Make it computed property
weak var coordinator: MainCoordinator? {
return MainCoordinator()
}
}
You could use objc associated objects.
extension UIViewController {
private struct Keys {
static var coordinator = "coordinator_key"
}
private class Weak<V: AnyObject> {
weak var value: V?
init?(_ value: V?) {
guard value != nil else { return nil }
self.value = value
}
}
var coordinator: Coordinator? {
get { (objc_getAssociatedObject(self, &Keys.coordinator) as? Weak<Coordinator>)?.value }
set { objc_setAssociatedObject(self, &Keys.coordinator, Weak(newValue), .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}
}
This happens because an extension is not a class, so it can't contain stored properties. Even if they are weak properties.
With that in mind, you have two main options:
The swift way: Protocol + Protocol Extension
The nasty objc way: associated objects
Option 1: use protocol and a protocol extension:
1.1. Declare your protocol
protocol CoordinatorProtocol: class {
var coordinator: MainCoordinator? { get set }
func menuTappedAction()
}
1.2. Create a protocol extension so you can pre-implement the addMenuButton() method
extension CoordinatorProtocol where Self: UIViewController {
func menuTappedAction() {
// Do your stuff here
}
}
1.3. Declare the weak var coordinator: MainCoordinator? in the classes that will be adopting this protocol. Unfortunately, you can't skip this
class SomeViewController: UIViewController, CoordinatorProtocol {
weak var coordinator: MainCoordinator?
}
Option 2: use objc associated objects (NOT RECOMMENDED)
extension UIViewController {
private struct Keys {
static var coordinator = "coordinator_key"
}
public var coordinator: Coordinator? {
get { objc_getAssociatedObject(self, &Keys.coordinator) as? Coordinator }
set { objc_setAssociatedObject(self, &Keys.coordinator, newValue, .OBJC_ASSOCIATION_ASSIGN) }
}
}
You can do it through subclassing
class CustomVC:UIViewController {
weak var coordinator: MainCoordinator?
func addMenuButton() {
let menuButton = UIButton(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
let menuImage = UIImage(named: "MenuWhite")
menuButton.setImage(menuImage, for: .normal)
menuButton.addTarget(self, action: #selector(menuTappedAction), for: .touchDown)
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: menuButton)
}
/// Launches the MenuViewController
#objc func menuTappedAction() {
coordinator?.openMenu()
}
}
class MainCoordinator {
func openMenu() {
}
}
class ViewController: CustomVC {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
}
Use a NSMapTable to create a state container for your extension, but make sure that you specify to use weak references for keys.
Create a class in which you want to store the state. Let's call it ExtensionState and then create a map as a private field in extension file.
private var extensionStateMap: NSMapTable<TypeBeingExtended, ExtensionState> = NSMapTable.weakToStrongObjects()
Then your extension can be something like this.
extension TypeBeingExtended {
private func getExtensionState() -> ExtensionState {
var state = extensionStateMap.object(forKey: self)
if state == nil {
state = ExtensionState()
extensionStateMap.setObject(state, forKey: self)
}
return state
}
func toggleFlag() {
var state = getExtensionState()
state.flag = !state.flag
}
}
This works in iOS and macOS development, but not on server side Swift as there is no NSMapTable there.
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)
}
}