In the iOS world, we're all used to the following pattern:
class UIViewController {
open var view: UIView!
}
A ViewController is obviously a controller controlling the view, which contains a lot of subviews.
Now, I have a lot of subviews that I want to reuse, and I want to enrich them with more functionalities. Think of a UISlider, a UILabel, or a UITableView that react to some events or some changes in the model. These subviews also need to be #IBDesignable with IBInspectable properties for customisation purposes. I also want to share those components through a library as well.
So in a way, I want small controllers controlling those subviews that will end up in the view of the ViewController.
I am thinking of doing this for the UIKit classes:
#IBDesignable
public class CustomSlider: UISlider {
}
That is a nice way to be able to provide the component with customisation options. The downside is that we're using inheritance here (would rather use composition), and I'm not sure if CustomSlider is really considered here a controller or not.
Can anyone tell me what are good practices around creating controllers for subviews that are customisable? Thanks in advance!
EDIT: Specific case for Views that have delegates and datasource:
#objc public class CustomTableView: UITableView, UITableViewDataSource, UITableViewDelegate {
#IBInspectable public var someCustomField: UInt = 0
public override init(frame: CGRect, style: UITableViewStyle) {
super.init(frame: frame, style: style)
dataSource = self
delegate = self
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
dataSource = self
delegate = self
}
// Implement UITableViewDataSource and UITableViewDelegate
}
Is this bad pattern to have datasource = self and delegate = self, or is it ok?
It's a judgement call. I'd say if all you are doing is adding features to a single view class, it is probably better to just use a custom subclass like your CustomSlider example.
If, on the other hand, you're using small suites of objects, like a slider, a text field, a segmented control and a couple of labels, you might want to think about using container views, embed segues, and custom view controllers. Think of that as setting up tiles that manage sets of UI elements. You can put such a view controller anywhere you want, and size it as needed.
You could also create a custom controller object that manages one or more custom views, but there isn't really system "plumbing" for that, so the burden is on you to build mechanisms to support that approach. You have to teach your view controllers to talk to a controller object that isn't a view, but that HAS views inside it.
Apple does what you're talking about in a couple of instances: UITableViewController and UICollectionViewController, and I think they didn't do it right. A UITableViewController or UICollectionViewController can manage only a single view. You can't put a button at the bottom, or a label somewhere, or a segmented control. The content view of one of those specialized view controllers must be the corresponding view object, which limits their usefulness. You can, of course, get around the problem by using container views and embed segues, and that leads me to my suggestion.
EDIT:
As far as making a view object it's own data source, I would call that "a violation of the separation of powers". A table view is a view object, and the data source is a model object. By combining them, you're collapsing the view and the model into one. That's going to create a larger, very specialized object that is less likely to be reusable.
The same goes for making an object it's own delegate. The idea of the delegate pattern is to be able to leave certain decisions about an object's behavior up to somebody else, so that its more flexible. By making it its own delegate you are limiting the table view's range of behavior and making it less reusable.
Neither thing is going to "warp your mind, curve your spine, and make the enemy win the war" but they seem ill-advised.
Related
I am trying to emulate Apple's weather app animation where when you scroll, the top header collapses and remains as a condensed sticky cell. I am attempting to do this with all of the UI being done programmatically and without storyboards.
The two ways I've thought of implementing this each have an issue I have not been able to solve.
My initial attempt was having a containerVC contain a UIView (as a header) and a UICollectionViewController. The issue is that The containerVC cannot access the UICollectionViews scrollViewDidScroll() which I would use to calculate and adjsut the size of the UIView. I could make the containerVC the collectionViews delegate but I wanted to avoid that to keep my logic separated. I also tried using Key Value observers but I could not figure out how to make it work.
My second attempt was to use a UICollectionReusableView as a header cell, that way there is no container view, just a single collectionViewController. The issue here is I can't figure out how to dynamically resize the headercell. The header size is currently being returned from referenceSizeForHeaderInSection and I have been unable to find another way of updating this.
Is there a better way to be going about this? Or an easier solution to the issues described that I haven't tried yet?
You can implement your own delegates:
protocol ParentDelegate: class {
func childDidScroll()
}
extension ParentViewController: ParentDelegate {
func childDidScroll() {
print("My child controller did scroll")
}
}
Create a delegate variable in the class that you want to call it:
weak var delegate: ParentDelegate?
Set it to your parent ViewController:
delegate = parentVC
then inside your child scroll method you call it:
delegate?.childDidScroll()
I have a UIViewController that is part of a UINavigationController and I want a custom view in my navigation bar. The code is then something like this:
class MyViewController: UIViewController
override func viewDidLoad() {
super.viewDidLoad()
self.customNavigationView = MyNavigationView()
self.navigationItem.titleView = self.customNavigationView
}
}
MyViewController and MyNavigationView are tightly coupled in a sense - they form a single piece of UI and only make sense when used together. I wonder what the preferred way to handle such cases is pattern-wise. In particular, what's the preferred way to send messages from MyNavigationView to MyViewController (such as buttons tapped).
I saw the following options:
Use a delegate pattern, create a MyNavigationViewDelegate protocol and add a delegate property to MyNavigationView. While the most object-oriented approach, it seems a bit "over-engineered" to me and has a lot of overhead for something that is basically a single unit of UI
Make MyNavigationView an inner class of MyNavigationController to indicate their strong relationship. Seems fine, but I like to have a file per class for easy navigation
Use a weak var myViewController: MyViewController? in MyNavigationView. I don't think the variable should be an optional, though, because semantically it is not
Use a let myViewController: MyViewController and a custom initializer in MyNavigationView that sets it. Seems the best option to me right now, but I'm not sure if it can create a memory leak since its a strong reference cycle
Thanks to #Mohsen Hossein pour: Sending notifications from MyNavigationView to MyViewController.
I wonder what other people's thoughts on this are. Is there are clear pattern that should be used here or is it a matter of taste?
While any answer to this question is ultimately opinionated in the MVP design pattern a view should not contain any business logic, just display logic. which means it should not know anything about its host.
The way I would approach it is to write the connection code and the interaction code in its host instead.
class MyNavigationView : UIView
{
var button : UIButton!
}
class ViewController : UIViewController
{
override func viewDidLoad() {
super.viewDidLoad()
self.customNavigationView = MyNavigationView()
self.navigationItem.titleView = self.customNavigationView
// here you can assign gesture recognizers or add event listeners
}
}
I would use a notification if you only need to know if something happened in the other view, it is easier and faster to implement, if you need more explanation on how to use notifications please ask and I can edit my answer and explain some more.
Say I have an app that has a fixed layout: one button and a few labels. It makes sense not to use multiple view controllers throughout the app since I have the same button and labels. I don't want to copy and paste them because their look and style will never change. If I decide to change their look later on, I would have to go through every single view controller and this is a bad practice (as it is with copying and pasting code).
Though, I want to be able to let the user go back and forward "layouts" the app, but this code doesn't let me do that:
class MyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
enterFirstLayout()
}
func enterFirstLayout() {
// do things such as change labels' text property
exitFirstLayout()
}
func exitFirstLayout() {
// do things
enterSecondLayout()
}
func enterSecondLayout() {
// ...
}
}
What can I do to avoid creating multiple view controllers and giving the user the possibility to skip and go back to a specific layout of the app?
I'm assuming that you are trying to achieve something like wizard that you can go back and foreword thru screens.
In this case I would use a UICollectionView with UICollectionViewFlowLayout. Build one custom cell that represents your single 'screen' layout than build a simple datasource as an array of custom objects. Datasource should contains for example all label's content as a String variables.
Nice and elegant code.
You get scrolling animation and swipe gesture detection for free. If you like you can add your custom fancy animation.
This is my solution. Going back to your question, if you want to do this your way, you can for example use subclassing. Build your base view controller class with all layout configured and labels exposed as public, read-only variables, then inherit from this class. Please keep in mind that subclassing is tightest possible coupling.
Other option is to build this view controller once and reuse with different dataset.
So decision is your. I would go in 1, 3, 2 order :)
I'm looking for a way to show a UIView "InventoryView" in 2 view controllers.
I'm working on an inventory system for my game that I trying to make but I need to be able to access it from my main view, where it will go to a InventoryViewController (in this ViewController is my InventoryView) but I also need to be able to access the InventoryView from my BattleViewController where it does not go to my InventoryViewController but where it print the InventoryView on my BattleViewController so I can access everything durning the battle.
Example:
(evrything is dragand drop, the UIView and the UIButtons)
InventoryViewController
class InventoryViewController: UIViewController {
class InventoryView: UIView {
//here are some UIButtons and labels
}
}
BattleViewController
class BattleViewController: UIViewController {
class InventoryView: UIView {
//it should print the Inventory Screen on my BattleViewController
//here are the same properties as it shows in the InventoryViewController
}
}
This is a great example to look at the way OOP programming works best.
Ask yourself the following questions:
What is the purpose of the view?
Are the interactions on the view homogenous across all the instances? (touch events, specific behavior, etc...)
What is the minimum amount of information you need to make the view look the way you want?
Once you have those answers, you can approach the concept of reusability of views safely.
The way to go about it is to subclass UIView, create the necessary elements of your view, setup your constraints (still in the view, either in a nib or programmatically), and implement any behavior that will be consistent across views (For example if the view is a segmented control, every time you click a segment all the others go grey and the one you clicked go blue. Since that's the primary purpose of the segmented control, the code for it should belong to the segmented control).
Chances are you will find the docs very useful: https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIView_Class/
Lastly write a setup method that takes all the information you need and sets up all your graphical elements accordingly. Remember, views should never own their data (they should be templates, the controller will provide the data).
I have no idea what you view looks like but I assume the inventory is represented as an object. Then something like could be a good start:
class InventoryView: UIView {
var inventory: Inventory? {
didSet {
if let newInventory = inventory { //in case we set it to nil
setup(withInventory: newInventory)
}
}
}
private func setup(withInventory inventory: Inventory) {
//do your setup here
}
}
Then in your controller you can call:
let inventoryView = InventoryView()
inventoryView.inventory = myPlayer.inventory
You cannot use a view in two places, at least not within the UI. Every view can be added to only one super view at a time.
If you need the same contents to be displayed twice, create a UIViewController class which's view contains the common UI, create two of those and add them to your UI.
I'm working on some custom UIView-based input controls, and I'm trying to ascertain proper practice for setting up the view. When working with a UIViewController, it's fairly simple to use the loadView and related viewWill, viewDid methods, but when subclassing a UIView, the closest methosds I have are `awakeFromNib, drawRect, and layoutSubviews. (I'm thinking in terms of setup and teardown callbacks.) In my case, I'm setting up my frame and internal views in layoutSubviews, but I'm not seeing anything onscreen.
What is the best way to ensure that my view has the correct height and width that I want it to have? (My question applies regardless of if I'm using autolayout, although there might be two answers.) What's the proper "best practice"?
Apple defined pretty clearly how to subclass UIView in the doc.
Check out the list below, especially take a look at initWithFrame: and layoutSubviews. The former is intended to setup the frame of your UIView whereas the latter is intended to setup the frame and the layout of its subviews.
Also remember that initWithFrame: is called only if you are instantiating your UIView programmatically. If you are loading it from a nib file (or a storyboard), initWithCoder: will be used. And in initWithCoder: the frame hasn't been calculated yet, so you cannot modify the frame you set up in Interface Builder. As suggested in this answer you may think of calling initWithFrame: from initWithCoder: in order to setup the frame.
Finally, if you load your UIView from a nib (or a storyboard), you also have the awakeFromNib opportunity to perform custom frame and layout initializations, since when awakeFromNib is called it's guaranteed that every view in the hierarchy has been unarchived and initialized.
From the doc of NSNibAwaking (now superseded by the doc of awakeFromNib):
Messages to other objects can be sent safely from within awakeFromNib—by which time it’s assured that all the objects are unarchived and initialized (though not necessarily awakened, of course)
It's also worth noting that with autolayout you shouldn't explicitly set the frame of your view. Instead you are supposed to specify a set of sufficient constraints, so that the frame is automatically calculated by the layout engine.
Straight from the documentation:
Methods to Override
Initialization
initWithFrame: It is recommended that you implement this method. You can also implement custom initialization methods in addition to,
or instead of, this method.
initWithCoder: Implement this method if you load your view from an Interface Builder nib file and your view requires custom
initialization.
layerClass Implement this method only if you want your view to use a different Core Animation layer for its backing store. For example,
if you are using OpenGL ES to do your drawing, you would want to
override this method and return the CAEAGLLayer class.
Drawing and printing
drawRect: Implement this method if your view draws custom content. If your view does not do any custom drawing, avoid overriding this
method.
drawRect:forViewPrintFormatter: Implement this method only if you want to draw your view’s content differently during printing.
Constraints
requiresConstraintBasedLayout Implement this class method if your view class requires constraints to work properly.
updateConstraints Implement this method if your view needs to create custom constraints between your subviews.
alignmentRectForFrame:, frameForAlignmentRect: Implement these methods to override how your views are aligned to other views.
Layout
sizeThatFits: Implement this method if you want your view to have a different default size than it normally would during resizing
operations. For example, you might use this method to prevent your
view from shrinking to the point where subviews cannot be displayed
correctly.
layoutSubviews Implement this method if you need more precise control over the layout of your subviews than either the constraint or
autoresizing behaviors provide.
didAddSubview:, willRemoveSubview: Implement these methods as needed to track the additions and removals of subviews.
willMoveToSuperview:, didMoveToSuperview Implement these methods as needed to track the movement of the current view in your view
hierarchy.
willMoveToWindow:, didMoveToWindow Implement these methods as needed to track the movement of your view to a different window.
Event Handling:
touchesBegan:withEvent:, touchesMoved:withEvent:, touchesEnded:withEvent:, touchesCancelled:withEvent: Implement
these methods if you need to handle touch events directly. (For
gesture-based input, use gesture recognizers.)
gestureRecognizerShouldBegin: Implement this method if your view handles touch events directly and might want to prevent attached
gesture recognizers from triggering additional actions.
This still comes up high in Google. Below is an updated example for swift.
The didLoad function lets you put all your custom initialization code. As others have mentioned, didLoad will be called when a view is created programmatically via init(frame:) or when the XIB deserializer merges a XIB template into your view via init(coder:)
Aside: layoutSubviews and updateConstraints are called multiple times for the majority of views. This is intended for advanced multi-pass layouts and adjustments when a view's bounds changes. Personally, I avoid multi-pass layouts when possible because they burn CPU cycles and make everything a headache. Additionally, I put constraint code in the initializers themselves as I rarely invalidate them.
import UIKit
class MyView: UIView {
//-----------------------------------------------------------------------------------------------------
//Constructors, Initializers, and UIView lifecycle
//-----------------------------------------------------------------------------------------------------
override init(frame: CGRect) {
super.init(frame: frame)
didLoad()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
didLoad()
}
convenience init() {
self.init(frame: CGRectZero)
}
func didLoad() {
//Place your initialization code here
//I actually create & place constraints in here, instead of in
//updateConstraints
}
override func layoutSubviews() {
super.layoutSubviews()
//Custom manually positioning layout goes here (auto-layout pass has already run first pass)
}
override func updateConstraints() {
super.updateConstraints()
//Disable this if you are adding constraints manually
//or you're going to have a 'bad time'
//self.translatesAutoresizingMaskIntoConstraints = false
//Add custom constraint code here
}
}
There's a decent summary in the Apple documentation, and this is covered well in the free Stanford course available on iTunes. I present my TL;DR version here:
If your class mostly consists of subviews, the right place to allocate them is in the init methods. For views, there are two different init methods that could get called, depending on if your view is being instantiated from code or from a nib/storyboard. What I do is write my own setup method, and then call it from both the initWithFrame: and initWithCoder: methods.
If you're doing custom drawing, you indeed want to override drawRect: in your view. If your custom view is mostly a container for subviews, though, you probably won't need to do that.
Only override layoutSubViews if you want to do something like add or remove a subview depending on if you're in portrait or landscape orientation. Otherwise, you should be able to leave it alone.
layoutSubviews is meant to set frame on child views, not on the view itself.
For UIView, the designated constructor is typically initWithFrame:(CGRect)frame and you should set the frame there (or in initWithCoder:), possibly ignoring passed in frame value. You can also provide a different constructor and set the frame there.