Swift: property that is a UIView subclass implementing a protocol - class

Here's the scenario. I have a number of different views I want to show, depending on the model object I'm showing the user. So I've set up a protocol whereby any view that implements it can be presented.
class MyItem { /* some model properties */ }
protocol ItemView: class {
// some protocol methods (e.g. updateWithItem(), etc)
func setupItem(item: MyItem)
}
class SpecificItemView: UIView, ItemView { /* there will be multiple classes like this */
func setupItem(item: MyItem) {
// do item setup
}
}
class AnotherItemView: UIView, ItemView {
func setupItem(item: MyItem) {
// do DIFFERENT item setup
}
}
Then when I go to use them in a view controller, I've been given one of my ItemView classes:
class MyViewController: UIViewController {
var itemView: ItemView? // could be a SpecificItemView or AnotherItemView
override func viewDidLoad() {
itemView?.setupItem(MyItem())
itemView?.removeFromSuperview() /* this line won't compile */
}
}
Everything works except that last line, where I try to call up to a UIView method (removeFromSuperview). No surprise there, since ItemView has no relation to UIView.
In Objective C I would solve this issue by specifying my itemView ivar like so:
#property (nonatomic, strong) UIView<ItemView> *itemView;
But I can't seem to find a similar syntax for Swift. I suspect there is no way to use this pattern in Swift. How do I achieve my overall goal of interchangeable UIView classes in a Swift-friendly manner?
One hacky solution I've found so far, is to add any UIView methods I call (e.g. removeFromSuperview) to my ItemView protocol.
Another suggestion I got, not on SO, from Maurice Kelly, was to make a UIView subclass, implementing the ItemView protocol, that both SpecificItemView and AnotherItemView can descend from. You can see it implemented in this gist. While that solves the issue of encapsulating class AND protocol in a single type (e.g. var itemView: ItemViewParentClass) it basically makes the protocol pointless, since you're now implementing all the protocol's methods in your parent class and overriding them in your subclasses. The single biggest drawback of this solutions is that you have to cast instances of subclasses (SpecificItemView) to the new hypthetical parent class (ItemViewParentClass) when you refer to them in your View Controller.

As of Swift 5, this is possible by constraining your protocol to only be applied to UIViews. To do this, just do the following:
protocol ItemView: UIView {
func setupItem(item: MyItem)
}
By adding this constraint to your protocol, any object that the swift compiler recognizes as an ItemView will also have all UIView methods available on it.
Similarly, if a non-UIView subclass attempts to implement the ItemView protocol, your code will not compile.

As you're discovering, there's no way to specify both a class and a protocol for an object.
The easiest way to fix the problem is to add removeFromSuperview (and any other UIView methods you need to insure are present) to your protocol.

Perhaps the following (in Swift 4) would do the trick?
class MyViewController: UIViewController {
typealias T = UIView & ItemView
var itemView: T?
}

ItemView does not extend UIView, but your SpecificItemView does. Naturally, no removeFromSuperView exists on your ItemView.
In your ViewController you declare an ItemView type object, rather than a SpecificItemView
Change your MyViewController code to:
var itemView:SpecificItemView?

Related

How to make `NSViewController` a `#MainActor` and have it implement `NSMenuItemValidation`

I've a custom NSViewController subclass that implements the NSMenuItemValidation protocol. That works just fine, until I mark the view controller as #MainActor.
I believed Apple marked all view controllers as main actor isolated already, but maybe it does so only on iOS. But I thought it would be safe to mark my subclass as such.
Once I add the #MainActor line, I get the warning Instance method 'validateMenuItem' isolated to global actor 'MainActor' can not satisfy corresponding requirement from protocol 'NSMenuItemValidation':
#MainActor
final class MyViewController: NSViewController, NSMenuItemValidation {
func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
}
}
Is it not okay to mark a custom NSViewController subclass as #MainActor? Or how should I deal with this warning?
I want to mark my view controller, because it needs to co-operate with other classes that are (explicitly) isolated to the main actor and I want to avoid various async boundaries that are totally not necessary, knowing the view controller will always run on the main thread.
You must mark the function w/ nonisolated. I'm assuming this is because the NSMenuItemValidation protocol is not restricted to the main queue.
nonisolated func validateMenuItem(_ menuItem: NSMenuItem) -> Bool { }

How do we return a value of collection view that is inside of tableview cell?

I have a table view cell that has a collection view inside of it, and i wanted to make a static method i can use from outside the class to get the value but i can not do that
what is an alternative way to return size of this collection view inside the table view cell
What I did is i stored a property with collection view height but couldn't return it in func
There are many ways to achieve this. One of the simplest ways it to use protocols.
In your CollectionViewCell but outside of your class, define protocol:
protocol CollectionViewCellDelegate: class {
func sizeOfCollectionView(size: CGSize)
}
In your CollectionViewCell class; define this protocol as a weak variable
weak var delegate: CollectionViewCellDelegate?
In your CollectionViewCell class; where you have the correct size; return it to any observers of this delegate using the following method:
self.delegate?.sizeOfCollectionView(size: bounds.size)
Now this delegate receives sizeOfCollectionView calls and it's parent can observe it.
For example:
In your parent class:
cell.delegate = self
After doing this try to build the project and Xcode will tell you that your class doesn't have the delegate implemented. You can then define the delegate as follows:
extension ParentClass: CollectionViewDelegate {
func sizeOfCollectionView(size: CGSize) {
// You will receive size here
}
}
Here I'm also adding a medium tutorial for a more detailed explanation on this.
Hope these help!

Making ViewController slimmer by moving TableView away

I am quite new into programming and facing some issues while trying to slim down my ViewController by moving creation of the tableView and associated views to the separate class and moving delegates and datasource from VC to separate one.
My constellation of current files and work is as follows:
After network connection, when data are received, I am calling inside a closure class to set view which will have embedded UITableView.
Fetched data are being saved into the CoreData stack
I instantiate view from another class
var detailView: DetailView! { return self.view as? DetailView }
Once I will download first part of the UI (separate call which works fine)
I am moving onto the part which is messy and surpass my abilities
I call a function createReposCard()
dispatchGroup.notify(queue: dispatchQueue) {
DispatchQueue.main.async {
// slide animation
UIView.animate(withDuration: 0.75, delay: 0.5, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.0, options: [], animations: {
self.detailView.reposCard.center = CGPoint(x: self.detailView.reposCard.center.x, y: self.detailView.reposCard.center.y-UIScreen.main.bounds.height)
}, completion: nil)
self.detailView.createReposCard(for: self.repos)
self.detailView.detailsTableView.reloadData()
self.activityIndicator.stopAnimating()
self.activityIndicator.removeFromSuperview()
}
}
Code is incomplete to depict the problem only, but what it does one by one.
I am passing info about selected user (irrelevant info)
then I am making a reference to my DetailsViewController which still e.g. holds CoreData stack
In initializer I am instantiating detailsViewController and passing it onto the class which holds delegates (I am passing it to have there reference to the CoreData)
class DetailView: UIView {
var selectedUser: User?
var detailsViewController: DetailsViewController!
let detailsTableView: UITableView = {
let tableView = UITableView()
tableView.frame = CGRect.zero
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "reposCell")
return tableView
}()
init(selectedUser:User, frame: CGRect) {
super.init(frame: frame)
self.selectedUser = selectedUser
detailsViewController = DetailsViewController()
let tableViewDelegates = TableViewDelegates(detailsViewController: detailsViewController)
detailsTableView.delegate = tableViewDelegates
detailsTableView.dataSource = tableViewDelegates
}
And finally code jumps into the depicted class where I am knocked down by "Unexpectedly found nil while implicitly unwrapping..."
public class TableViewDelegates: NSObject, UITableViewDataSource,UITableViewDelegate {
private let detailsViewController: DetailsViewController
init(detailsViewController: DetailsViewController){
self.detailsViewController = detailsViewController
super.init()
}
public func numberOfSections(in tableView: UITableView) -> Int {
return detailsViewController.fetchedResultsController.sections?.count ?? 1
}
...
Actually I don't know if my concept is good as I feel quite lost, but my intentions were as follows:
Move View creation to other class
Move TableView delegates to other class
Finally - move networking and CoreData to other class.
But as I see, simple data passing is overcoming my abilities.
I think this question can be divided into two parts:
1) Why is my variable nil when I unwrap it?
I don't think we have enough information to answer this accurately, but my overall approach would be like this:
Check what nil variable is being unwrapped;
Make sure this variable is being properly initialized;
Make sure that the object is not being incorrectly deinitialized;
If you're using Storyboard, use the inspectors to check if everything is set correctly.
There's a particular observation about step 2: you should check the order of execution of your methods to make sure that the variable is properly initialized. Why am I emphasizing this? Because there's a chance that some view (e.g., detailView) is initialized like an ordinary UIView, and then you try to access an element that is not part of a UIView object (e.g., a table view). In other words, check if you're setting the custom views before you try to access them.
2) How to structure the project in a more organized way?
This is a more interesting question, and I think that choosing a better approach will help you to avoid issues like what you're experiencing. I will divide this into some topics. Everything here is my personal opinion and doesn't necessarily reflect the best approach, especially because "best" is subjective here.
PersistenceManager class
First, passing a reference of a view controller to another class just to access CoreData doesn't seem like a good option. A better approach would be to have a PersistenceManager class, for example. You could use an object of this class to fetch and save data. You could pass this object instead of the view controller.
In some architectures (e.g., VIPER), it wouldn't be correct for the view controller to access the persistence directly, so it would be more appropriate to pass an array of already fetched objects. For example:
class TableViewController {
private let tableView: UITableView!
private var currentlyDisplayedUsers: [Users]?
func displayUsers(_ users: [Users]) {
self.currentlyDisplayedUsers = users
self.tableView.reloadData()
}
}
In the example above, the tableView would display currentlyDisplayedUsers, which would be updated by the method displayUsers, which would be called by someone else, like a Presenter.
Network class
Second, I think you should have a network class to download data from the internet. You would use instances of the Network class in the application logic. In other words, you would have something like
// This is in the application logic
// In MVC, the logic is in the Controller, while in VIPER the logic is in the Interactor
class ApplicationLogic {
let networkAPI: Network?
...
func fetchUserData() {
networkAPI?.fetchUsers() { data, error in
// Update the UI based on the response
// Using the previous example, you could call displayUsers here
}
}
}
TableView, TableViewDelegate, and TableViewDataSource
Finally, how to organize these guys. UITableViewDelegate is responsible for telling us about events in the table, while UITableViewDataSource is responsible for filling the table with data, which means that both are strongly related to the table view itself. That said, imho, both should be implemented in different swift files, but as extensions of the view controller that has a reference to the table view. Something like
// TableViewController+UITableViewDelegate.swift
extension TableViewController: UITableViewDelegate {
...
}
// TableViewController+UITableViewDataSource.swift
extension TableViewController: UITableViewDataSource {
...
}
Using this approach, the delegate and the data source would have access to the users array, mentioned earlier. Here is an example of how to implement a similar approach using VIPER.
So, I hope I could provide a basic idea on these topics. If you want to understand more about how to structure your code, I suggest researching iOS design patterns and architectural patterns. Some architectural design patterns that are famous in iOS development are MVC, MVP, MVVM, and VIPER.

performSegueWithIdentifier from a NSView subclass?

I have a document window that contains a number of NSView subclasses, switched between using a tab control. Each of the subclasses, and the window's ViewController, support different user actions accessed through menu items tied to the First Responder.
I'd like to perform a segue from one of those views in response to a menu item. However, NSView does not support performSegueWithIdentifier, it appears to be something that is part of NSViewController alone.
Can someone suggest a way around this? I have seen suggestions to pass the VC into the views, but I am not clear how to do that. Or perhaps there is a better way?
view.containingController.performSegue()
note: you have to add containingController to your views
I WOULD add the viewController to the responder chain and then make containingController a computed property in an extension!
e.g. add vc as responder:
override func awakeFromNib() {
super.awakeFromNib()
self.nextResponder = self.view
for subview in self.view.subviews {
subview.nextResponder = self
}
}
e.g. containingController in extension
extension NSView {
var containingController: NSViewController? {
get {
while(self.nextResponder != nil) {
if(self.nextResponder is NSViewController) {
return self.nextResponder
}
}
return nil
}
}
}
You could do that (see Daij-Djan's answer), however it is not what I would recommend, since a hypothetical programmer who will be using your code, but is not familiar with it (let's say, you in a year :) ) might be caught by surprise by such behaviour.
I would recommend you to add a delegate (conforming to your custom protocol, let's call it MyViewDelegate) to your NSView with a method like viewRequiresToPerformTransition(view: YourViewSubclass). Then you implement this method (more generally, you conform to MyViewDelegate protocol) in your view controller and inside its implementation perform any segue you want.

How do I handle callbacks when using selectors?

I'm placing my logic code in a viewModel. The view calls one method in the viewController. That method then calls the rest of the methods in the viewModel by using #selectors. This works fine up until the tableView needs to be reloaded with tableView.reloadData(). That part obviously needs to be in the view.
Normally, this would be accomplished by using multiple closures. But as #selectors can't have parameters I can't have a completion() callback in the last method that is called. So, my question is, how do I get around this problem? Is there any good alternatives to using #selectors? Should I have an observer in the view subscribing to the last method of the viewModel? Is RxSwift an alternative? Or is there a workaround using #selectors?
RxSwift is a good alternative, but in case you need something not as heavy, the delegate pattern is what you need:
protocol ViewDelegate {
// Other functions you might need
func reloadTableView()
}
Then in your viewController, you implement these:
class ViewController: ViewDelegate {
func reloadTableView() {
tableView.reloadData()
}
}
And somewhere, in your view model you need to define the delegate:
weak var viewDelegate: ViewDelegate
As well as assign it when creating the classes:
let model = ViewModel()
let view = ViewController()
model.viewDelegate = view
Swift official documentation has a lot more on protocols: https://docs.swift.org/swift-book/LanguageGuide/Protocols.html