I have a UITextView created in storyboard. The view uses TextKit 1, and I'm trying to replace the NSLayoutManager. When calling .sizeThatFits() (or any other method requiring layout) on my text view, I'm getting an uncaught exception:
*** -[NSLayoutManager glyphRangeForTextContainer:]: given container does not appear in the list of containers for this NSLayoutManager.
A minimal reproducible example project.
I'm replacing the layout manager in my text view subclass:
let layoutManager = MyLayoutManager()
layoutManager.textStorage = self.textStorage
layoutManager.addTextContainer(self.textContainer)
self.textContainer.replaceLayoutManager(layoutManager)
// etc.
I've tried doing it in multiple places, in both initWithCoder and awakeFromNib, and made sure the layout manager actually does contain the NSTextContainer. The crash happens even when using plain NSLayoutManager, not a subclass.
When looking at the stack trace, I'm seeing calls to private TextKit 1 methods:
Before entering .sizeThatFits(), the container is included in container array:
if (layoutManager.textContainers.contains(self.textContainer) {
// true
}
I'm unable to tell from the actual assembly calls if the private methods really use my own layout manager, but glyphRangeForTextContainer: in my subclass is not called from these methods.
Could it be that somehow the private methods find a remnant of the old layout manager in memory, even if it shouldn't be retained by anything?
This also seems to happen in an empty project when using TextKit 1, so it might be a bug, but I want to rule out my user error first.
It turns out you need to override var layoutManager (or -(NSLayoutManager*)layoutManager in ObjC) in your text view. The getter does not return the layout manager associated with the text container, which is a bit weird.
class TextView:UITextView {
var customLayoutManager:NSLayoutManager = NSLayoutManager()
required init?(coder: NSCoder) {
super.init(coder: coder)
customLayoutManager.textStorage = self.textStorage
customLayoutManager.addTextContainer(self.textContainer)
self.textContainer.replaceLayoutManager(customLayoutManager)
}
override var layoutManager: NSLayoutManager {
return customLayoutManager
}
}
Related
I have a
#IBDesignable
class Fancy:UIButton
I want to
addTarget(self, action:#selector(blah),
forControlEvents: UIControlEvents.TouchUpInside)
So where in UIButton should that be done?
Where is the best place for addTarget ?
1 - I have seen layoutSubviews suggested - is that right?
Note - experimentation shows that a problem with layoutSubviews is that, of course, it can be called often, whenever things move around. It would be a bad idea to "addTarget" more than once.
2 - didMoveToSuperview is another suggestion.
3 - Somewhere in (one of) the Inits?
Note - experimentation shows a fascinating problem if you do it inside Init. During Init, IBInspectable variables are not yet actually set! (So for example, I was branching depending on the "style" of control set by an IBInspectable; it plain doesn't work as #IBInspectable: won't work when running!)
4 - Somewhere else???
I tried to do it in Init, and it worked well. But it breaks designables from working in the Editor.
By thrashing around, I came up with this (for some reason both must be included?)
#IBDesignable
class DotButton:UIButton
{
#IBInspectable var mainColor ... etc.
required init?(coder decoder: NSCoder)
{
super.init(coder: decoder)
addTarget(self, action:#selector(blah),
forControlEvents: UIControlEvents.TouchUpInside)
}
override init(frame:CGRect)
{
super.init(frame:frame)
}
I don't know why that works, and I don't understand why there would be two different init routines.
What's the correct way to include addTarget in a UIButton?
tl;dr
override func endTrackingWithTouch(touch: UITouch?, withEvent event: UIEvent?) {
super.endTrackingWithTouch(touch, withEvent: event)
if let touchNotNil = touch {
if self.pointInside(touchNotNil.locationInView(self), withEvent: event) {
print("it works")
}
}
}
Why not use addTarget
addTarget method is part of action-target interface which is considered 'public'. Anything with reference to your button can, say, remove all of its actions, effectively breaking it. It is preffered to use some of 'protected' means, for instance endTrackingWithTouch which is accessible only to be overriden, not called directly. This way it will not interfere with any external objects using action-target mechanism.
(I know there is no strict 'public' or 'protected' in ObjC/UIKit, but the concept remains)
Your way
If you want to do it exactly your way then your example is all good, just copy addTarget call to init(frame:CGRect).
Or you can put addTarget in awakeFromNib (don't forget super) instead of init?(coder decoder: NSCoder), but you will be forced to implement init with coder anyway, so...
layoutSubviews and didMoveToSuperView both are terrible ideas. Both may happen more than once resulting in blah target-action added again. Then blah will be called multiple times for a single click.
By the way
The Apple way
By the Cocoa MVC (which is enforced by UIKit classes implmentation) you should assign that action to the object controlling that button, animations or not. Most often that object will be Cocoa MVC 'Controller' - UIViewController.
If you create button programmatically UIViewController should assign target to itself in overridden loadView or viewDidLoad. When button is loaded from nib the preffered way is to assign target action in xib itself.
Old Good MVC
As mentioned here in real MVC views do not send actions to themselves. The closest thing to real MVC Controller in UIKit is UIGestureRecognizer.
Be warned that it's pretty difficult to pull of real MVC with UIKit class set.
You should not add as target the same object that produces the action.
The target and its callback should be another object, usually a view controller.
There are 2 inits methods because the button can be instantiated by calling init or by the process of deserializion (NSCoder) from a nib/xib. Since you probably added the button to a storyboard the init method called is init?(_: NSCoder).
[UPDATE]
I agree about what you say in the comment, but I think that the action-target pattern should be used for communicating with other objects, I'm using conditional, because as far as I know I never seen something like what you wrote in Apple code or some other library. If you want to intercept and make some actions inside the button you should probably override some of the methods exposed in UIControl.
About designable, you are, again, correct. init(frame) is called if you are creating a button programmatically, init(coder) if the button comes from a xib.
The method init(frame) is also called during designable process. At this point I think that the best option is to debug directly your view.
Place some breakpoints inside you UIButton subclass
Select the view in your storyboard
Go to the Editor -> Debug selected views
Now you should be able to understand where the problem is.
How about implementing awakeFromNib and doing it there?
https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Protocols/NSNibAwaking_Protocol/#//apple_ref/occ/instm/NSObject/awakeFromNib
You can also conditionally run (or not run) code when it is being run in the context of Interface Builder:
#if !TARGET_INTERFACE_BUILDER
// this code will run in the app itself
#else
// this code will execute only in IB
#endif
(see http://nshipster.com/ibinspectable-ibdesignable/)
Your initialize method is not correct, this will work:
```swift
override init(frame: CGRect) {
super.init(frame: frame)
self.loadNib()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.loadNib()
}
private func loadNib() {
let nibView = NSBundle(forClass: self.classForCoder).loadNibNamed("yourView", owner: self, options: nil).first as! UIView
nibView.frame = self.bounds
nibView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
self.button.addTarget(self, action: #selector(action), forControlEvents: .TouchUpInside)
self.addSubview(nibView)
}
```
I've had this working in other variations but something seems to elude me in the change from objective-c to swift as well as moving some of the setup into it's own class.
So i have:
class ViewController: UIViewController, interfaceDelegate, scrollChangeDelegate{
let scrollControl = scrollMethods()
let userinterface = interface()
override func viewDidLoad(){
super.viewDidLoad()
loadMenu("Start")
}
func loadMenu(menuName: String) {
userinterface.delegate = self
userinterface.scrollDelegate = self
userinterface.removeFromSuperview() //no impact
scrollControl.removeFromSuperview() //no impact
userinterface.configureView(menuName)
view.addSubview(scrollControl)
scrollControl.addSubview(userinterface)
}
}
This sets everything up correctly but the problem occurs when I change loadMenu() at runtime. So if the user calls loadMenu("AnotherMenu") it won't change the UIView. It will call the right functions but it won't update the view. Although if I call loadMenu("AnotherMenu") at the start, the correct menu will display. Or if I call loadMenu("Start") and then loadMenu("AnotherMenu") then the menu displayed will be "AnotherMenu". As in:
override func viewDidLoad(){
super.viewDidLoad()
loadMenu("Start")
loadMenu("AnotherMenu")
}
When I list all the subviews each time loadMenu() is called, they look correct. Even during runtime. But the display is not updated. So something isn't getting the word. I've tried disabling Auto Layout after searching for similar issues but didn't see a difference.
Try adding setNeedsDisplay() to loadMenu
Eg
func loadMenu(menuName: String) {
userinterface.delegate = self
userinterface.scrollDelegate = self
userinterface.removeFromSuperview() //no impact
scrollControl.removeFromSuperview() //no impact
userinterface.configureView(menuName)
view.addSubview(scrollControl)
scrollControl.addSubview(userinterface)
view.setNeedsDisplay()
}
setNeedsDisplay() forces the view to reload the user interface.
I didn't want to post the whole UIView class as it is long and I thought unrelated. But Dan was right that he would need to know what was going on in those to figure out the answer. So I created a dummy UIView class to stand in and intended to update the question with that. I then just put a button on the ViewController's UIView. That button was able to act on the view created by the dummy. So the problem was in the other class. Yet it was calling the methods of the ViewController and seemingly worked otherwise. So then the issue must be that its acting on an instanced version? The way the uiview class worked, it uses performSelector(). But in making these methods into their own class, I had just lazily wrote
(ViewController() as NSObjectProtocol).performSelector(selector)
when it should have been
(delegate as! NSObjectProtocol).performSelector(selector)
so that was annoying and I wasted the better part of a day on that. But thanks again for the help.
I'm using a custom UICollectionViewCell class for my UICollectionView. I have to use addSubview in my custom class because I'm using FirebaseUI-iOS. This is what my MessageCollectionViewCell looks like:
import Foundation
import UIKit
class MessageCollectionViewCell: UICollectionViewCell {
#IBOutlet var messageContainerView: UIView?
#IBOutlet var messageText: UILabel?
#IBOutlet var messageDisplayName: UILabel?
#IBOutlet var messageUserImage: UIImageView?
#IBOutlet var messageUserImageOverlay: UIView?
override init(frame: CGRect) {
super.init(frame: frame)
// Custom initialization code for label
let size = self.contentView.frame.size
let frame = CGRectMake(0.0, 0.0, size.width, size.height)
self.messageContainerView = UIView(frame: frame)
self.messageUserImageOverlay = UIView(frame: frame)
self.messageText = UILabel(frame: frame)
self.messageDisplayName = UILabel(frame: frame)
self.messageContainerView!.addSubview(self.messageUserImageOverlay!)
self.messageContainerView!.addSubview(self.messageText!)
self.messageContainerView!.addSubview(self.messageDisplayName!)
self.contentView.addSubview(messageContainerView!)
}
required init(coder aDecoder: NSCoder) {
println("Init")
super.init(coder: aDecoder)
}
}
I have constraints in my Storyboard file which I want to use, but when I'm using addSubview my constraints does not get used. Is there anyway I can use addSubview() and still keep the constraints? I know that I can add it programmatically, but I wish to use the constraints that i've already set inside Storyboard.
This issue is due to how FirebaseUI 'FirebaseCollectionViewDataSource` registers it's classes. I don't believe it's possible to instantiate a class like this and get the autolayout properties from a XIB like you're asking, but it is possible to solve the problem a layer back by fixing our handling of prototype cells.
The problem here is that we're registering the cell reuseIdentifier twice: once in the storyboard and once in code (FirebaseCollectionViewDataSource must do this in order to dequeue cells). Since we call ours second, it overwrites the first one, which means that none of your outlets are populated, layouts are weird, etc. This means that you have to set them up as if you were using regular subclasses rather than XIBs. The quickest thing you could do here is just use a XIB instead of a prototype cell (which is just a XIB inside the storyboard). So, how can we support FirebaseUI + prototype cells...
The short answer is that currently this feature isn't possible due to Apple's design of UICollectionView.
Unlike UITableView, which can check for this behavior by dequeuing a cell (which would return an instantiated prototype cell and tell us that the reuseidentifier has already been created) like we do in FirebaseTableViewDataSource here, UICollectionView doesn't provide a similar method, it only gives:
func dequeueReusableCellWithReuseIdentifier(_ identifier: String,
forIndexPath indexPath: NSIndexPath!) -> AnyObject
Given that this method requires an a indexPath as it has to return a non-nil object, it will throw an NSInternalInconsistencyException when we try to read an arbitrary object at initialization (since there exist no items for it to read from). Additionally, there doesn't appear to be any way to programmatically check if a reuseIdentifier is in use. This leaves us with a few options:
Recommend people not use prototype cells and instead do custom subclasses or XIBs and hook them in. Storyboards and prototype cells feel a little more brittle (mostly for reasons like this), but definitely have ease of use going for them.
Pull the -registerClass: forReuseIdentifier: call out of FirebaseCollectionViewDataSource, though this means that FirebaseTableViewDataSource should be changed as well (even though it can work) and make the developer explicitly call this (or not in the case of using a Storyboard).
Add a parameter for storyboards to the initialization call which would still retain the reuseIdentifier to dequeue cells, but not register the class.
Try to dequeue a cell, catch the NSException, register the class and try again. This works, but it throws the exception still and adds a little more code in the runloop (we have to wrap the call with try-catch that we know will fail at most once).
My personal preference is 1, but the value proposition for prototype cells is high enough that 3 might be the best option for this library. I'd rather not do 2, since cell registration is a difficult enough problem.
For now, I'd recommend using XIBs instead of prototype cells, or waiting for us to pick one of the above solutions (we can push a release pretty quickly to solve the problem).
I'm new to Swift, come from Obj-C
I'm looking at the net, but to no avail ....
Obj-C in my project I created a class UIView (served as a notification that appeared red from the bottom) with a custom instancetype because through this allocavo the UIView directly in viewController with the parameters that I was interested .. Now I can not absolutely to create the translation of my code in Obj-C swift was trying to find a tutorial or something visual that I could figure out how to create a class with swift UIView without using a xib ...
Does anyone know any tutorial or example of class UIView without XIB in Swift?
My problem is that when I insert this custom init
init (title:String, message:String) {
// init
}
first of all it returns an error because the compiler wants this also included
required init (coder aDecoder: NSCoder) {
super.init (coder: aDecoder)
}
My first question is this:
initWithCoder in obj-c was used only if my custom view was inserted in the storyboard ...
however in my case I would call all my custom view, in my view controller, through a simple line of code as I did in obj-c and that I was not allowed in because obj-c I used a custom instancetype without specifying them a initWithCoder it initWithFrame
I can not understand how this thing
To declare a class as a subclass of another class:
class MyView : UIView {
// properties, methods such as `drawRect:`, etc., go here
}
To instantiate that class:
let v = MyView()
To configure that instance:
v.frame = // ....
To put a view instance into the interface:
self.view.addSubview(v)
I have an already setup view and want to wrap it in a subclass of UIView.
class ElementView: UIView {
var selected = false
}
The problem is that I cannot initialize ElementView with the already existing view.
Swift doesn't allow assigning to self too.
init(view: UIView) {
//here I would have to call super.init(coder: NSCoder?) or super.init(frame: CGRect)
//none of which actually properly initializes the object
}
Any ideas how to implement this?
Thanks :)
Clarification:
I will give you the larger context hoping it'd be more clear:
I am implementing a UIScrollView subclass. The scroll view contains an array of UIView objects which are added externally (from the user of the class).
In my custom UIScrollView class I want to implement a tap gesture recognizer for each object. That's already done:
let singleTap = UITapGestureRecognizer(target: self, action: Selector("handleTap:"))
singleTap.cancelsTouchesInView = false
addGestureRecognizer(singleTap)
and the handler:
func handleTap(recognizer: UITapGestureRecognizer) {
//some code to handle the tap
}
The problem arises when I want to handle the tap, I want to store the previous state of the view (was it tapped before or not) in order to toggle that state when the tap happens. I want to do different stuff to the UIView depending on its state.
recognizer.view returns the view to which the recognizer is attached to, which is what I need. But, this way I have no possibility of implementing a state for the UIView.
That's why I wanted to implement a custom wrapper for UIView which should contain the state information (which is also a problem). That's how I came up to asking this question...
In order to create a custom init for the UIView subclass you have to call the required init for the UIView superclass. Also while creating the custom init you have to send the Frame of the view to the superclass. Upon fulfilling these requirements you are free to pass on any arguments to the newly created init including the tap recognizer info.
Remember, if you are creating any variables - they have to be not nil upon the creation of the instance, thus in variable declaration you have to create some initial argument (for example - 0 for Int, etc.) for the initializer to work. Here is the example code:
var variableOne: Int = 0
var variableTwo: Int = 0
init(variableOne: Int, variableTwo: Int) {
self.variableOne = variableOne
self.variableTwo = variableTwo
super.init(frame: CGRectZero)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
It sounds like you are trying to mimic a copy constructor, and it sounds like you are trying to build it in IB. The short answer is what you are doing doesn't make sense. It further sounds like that you wanted your code above to own or assume the identity of the argument view (your reference to not being able to assign to self). If this assumption is correct, your code would make even less sense - you would just need to assign to a variable.
Just create the view class, with the code that you have posted, you do not need to implement a constructor, since you have provided a default value for your selected variable. Your will with then be populated from IB via the coder based constructor.
If you are trying to clone or copy a given view, then refer to "Can UIView be copied"