editable nstextview from interface builder - swift

I'm unable get to edit an nstextview.
Here is the steps I tried.
From the interface builder, I dragged dropped a "Custom view" into another view. (I couldnt find a nstextview there.)
I changed the class of the Custom view form "NSView" to "NSTextView"
Next I run my project, I can see the text view getting rendered (the mouse cursor changed to text-mode on the mouse-hover of text view area)
However, I have not been able to insert/type/edit any text.
Note: I have tried setEditable, setSelectable & set firstResponder options recommended in other posts, which did not help.

The problem is that the textStorage is not set up correctly on the NSTextView when you use Interface Builder in the way described.
I too wanted to use an NSTextView, without scrollView, in InterfaceBuilder. In Xcode 10 it doesn't seem possible to put a lone NSTextView in some custom view hierarchy (earlier answers have hinted that this was possible: https://stackoverflow.com/a/2398980/978300).
It is possible to get this working with the "CustomView" method in the question - however, this will only have the simple NSView properties in the Attributes inspector (i.e. you won't be able to customise font etc). You could pass some details through using #IBInspectable on your custom class.
The Connections Inspector seems to work correctly.
Example NSTextView subclass...
class MyTextView: NSTextView
{
// init(frame: sets up a default textStorage we want to mimic in init(coder:
init() {
super.init(frame: NSRect.zero)
configure()
}
/*
It is not possible to set up a lone NSTextView in Interface Builder, however you can set it up
as a CustomView if you are happy to have all your presentation properties initialised
programatically. This initialises an NSTextView as it would be with the default init...
*/
required init(coder: NSCoder) {
super.init(coder: coder)!
let textStorage = NSTextStorage()
let layoutManager = NSLayoutManager()
textStorage.addLayoutManager(layoutManager)
// By default, NSTextContainers do not track the bounds of the NSTextview
let textContainer = NSTextContainer(containerSize: CGSize.zero)
textContainer.widthTracksTextView = true
textContainer.heightTracksTextView = true
layoutManager.addTextContainer(textContainer)
replaceTextContainer(textContainer)
configure()
}
private func configure()
{
// Customise your text here...
}
}

Related

Scrollable NSTextView with custom NSTextStorage for formatting

I'm trying to make a text editor with formatting for Mac OS. Which I have working using an NSTextView together with a custom NSTextStorage class. Which applies attributes like bold etc to NSAttributableStrings.
This all seems to work fine as seen in screenshot one below. Which is an NSTextView with a custom NSTextStorage class attached to it. Which applies the formatting through attributes on an NSAttributeableString
However, having everything the same, but getting a scrollable NSTextView from the Apple supplied function NSTextView.scrollableTextView() it does not display any text at all. Even though you can see in the screenshot that the NStextView is actually visible. Also, moving my mouse over the editor changes the cursor to the editor cursor. But I can't select, type or do anything.
Doing the exact same thing as above, but not supplying a text container to the text view does show that it is wired up correctly, since I do get a scrollable text view then. Where the scrolling actually works, but then of course the formatting is no longer applied.
So I'm confused on what I have to do now.
This is basically my setup:
//
// TextDocumentViewController.swift
//
// Created by Matthijn on 15/02/2022.
// Based on LayoutWithTextKit2 Sample from Apple
import Foundation
import AppKit
class TextDocumentViewController: NSViewController {
// Extends NSTextStorage, applies attributes to NSAttributeAbleString for formatting
private var textStorage: TextStorage
// Not custom yet, default implementation - do I need to subclass this specifically and implement something to support the scrolling behaviour? Which seems weird to me since it does work without scrolling support
private var layoutManager: NSLayoutManager
// Also default implementation
private var textContainer: NSTextContainer
private var textDocumentView: NSTextView
private var scrollView: NSScrollView
required init(content: String) {
textStorage = TextStorage(editorAttributes: MarkdownAttributes)
layoutManager = NSLayoutManager()
textStorage.addLayoutManager(layoutManager)
textContainer = NSTextContainer()
// I'm not 100% sure if I need this on false or true or can just do defaults. No combination fixes it
// textContainer.heightTracksTextView = false
// textContainer.widthTracksTextView = true
layoutManager.addTextContainer(textContainer)
scrollView = NSTextView.scrollableTextView()
textDocumentView = (scrollView.documentView as! NSTextView)
// Basically commenting this out, stops applying my NSTextContainer, NSLayoutManager and NSTextContainer, but then of course the formatting is not applied. This one line changes it between it works without formatting, or it doesn't work at all. (Unless I have my text view not embedded in a scroll view) then it works but the scrolling of course then does not work.
textDocumentView.textContainer = textContainer
textDocumentView.string = content
textDocumentView.isVerticallyResizable = true
textDocumentView.isHorizontallyResizable = false
super.init(nibName: nil, bundle: nil)
}
override func loadView() {
view = scrollView
}
}
You can actually create the NSScrollView instance with scrollableTextView() and you can get the implicitly created documentView (NSTextView).
Finally, one can assign the existing LayoutManager of the documentView to the own TextStorage class inheriting from NSTextStorage.
In viewDidLoad you could then add the scrollView traditionally to the view using addSubview:. For AutoLayout, as always, translatesAutoresizingMaskIntoConstraints must be set to false.
With widthAnchor and a greaterThanOrEqualToConstant you can define a minimum size, so the window around it cannot be made smaller by the user. The structure also allows potential later simple extensions with additional sticky views (e.g. breadcrumb view etc).
Code
If you implement it this way, then for a small minimal test it might look something like this.
import Cocoa
class ViewController: NSViewController {
private var scrollView: NSScrollView
private var textDocumentView: NSTextView
required init(content: String) {
scrollView = NSTextView.scrollableTextView()
let textDocumentView = scrollView.documentView as! NSTextView
self.textDocumentView = textDocumentView
let textStorage = TextStorage(editorAttributes: MarkdownAttributes())
textStorage.addLayoutManager(textDocumentView.layoutManager!)
textDocumentView.string = content
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
scrollView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(scrollView)
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.widthAnchor.constraint(greaterThanOrEqualToConstant: 200.0),
scrollView.heightAnchor.constraint(greaterThanOrEqualToConstant: 200.0),
])
}
override func loadView() {
self.view = NSView()
}
}
Test
Here's a quick test showing that both display and scrolling work as expected with the setup:

Set Accessibility Identifier in UIView Extension

I worked on Mobile Test Automation.Previous, some elements don't have any identifier but i need to import identifiers for testing issues.
So I decide to write an extension to UIView, hereby that code will be affect all codes so I wont need to add one by one.
How can I do ? Should I write on init or awakeFromNib ?
Thanks in advance.
Generally you'll want to have specific accessibility identifiers for elements you want to expose to the accessibility system.
You can set those directly in Storyboards/Interface Builder, or you can set them in your view's initializer when implementing UIs programatically:
class MyView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
self.accessibilityIdentifier = "my-custom-view"
let label = UILabel()
label.accessibilityIdentifier = "my-custom-label"
self.addSubview(label)
}
}

Any way to opt out of autoresizing permanently?

I'm writing nib-less views in which I use autolayout for all my layout logic. I find myself having to turn off autoresizing with every view I instantiate. My code is littered with a lot of these:
view.translatesAutoresizingMaskIntoConstraints
Ideally I'd like to just
extension UIView/NSView {
override var translatesAutoresizingMaskIntoConstraints: Bool = false
}
and get it over with once and for all, but extensions can't override stored properties.
Is there some other simple way to switch off autoresizing for good?
Well just a suggestion since its annoying to always set that to false, just setup a function with all the shared setups for the UIView and call it every time,
its saves time and its kinda less annoying than trying and setting the values each time,
extension UIView {
func notTranslated() {
self.translatesAutoresizingMaskIntoConstraints = false
//Add any additional code.
}
}
//Usage
let view = UIView()
view.notTranslated()
You can't override this constraints properties because the UIView maybe declared in the IB
translatesAutoresizingMaskIntoConstraints according to apple.
By default, the property is set to true for any view you programmatically create. If you add views in Interface Builder, the system automatically sets this property to false.
imagine if you could override that from an extension that would lead to some conflicts if there was other UIView's that's have the opposite value True || false, so in my opinion:
Apple did this to prevent any conflicts with the views constrains, therefore if you don't like to write it every time just wrap it up in a function.
Please if anyone have additional information, don't hesitate to contribute.
UPDATE: I found this cool answer that could also work, check out the code below.
class MyNibless: 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: CGRect.zero)
}
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
translatesAutoresizingMaskIntoConstraints = false
//Add custom constraint code here
}
}
var nibless: UIView = MyNibless()
//Usage
nibless.updateConstraints()
print(nibless.translatesAutoresizingMaskIntoConstraints) //false
So simply just create MyNibless instance as UIView and this also open big door to customizations too

Is IBDesignable broken for AppKit in Xcode 9.2 and Swift 4?

Most questions, and answers related to this, are based on older versions of both Xcode and Swift. Additionally, 90 percent of the questions relate to UIKit and drawing custom controls.
I am adding a standard button, that is centered inside a custom control, decorated with IBDesignable.
import Cocoa
#IBDesignable public class ButtonPresetView: NSView {
public override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
initialControlSetup()
}
public required init?(coder decoder: NSCoder) {
super.init(coder: decoder)
initialControlSetup()
}
private func initialControlSetup() {
let button = NSButton(title: "Hello", target: nil, action: nil)
button.translatesAutoresizingMaskIntoConstraints = false
addSubview(button)
// Configure button
centerXAnchor.constraint(equalTo: button.centerXAnchor).isActive = true
centerYAnchor.constraint(equalTo: button.centerYAnchor).isActive = true
}
}
I add a custom view to the application and set the class property in the Identity Inspector to my custom class (ButtonPresetView).
It should show the button centered on the canvas, but the canvas is blank.
Not sure many people use it this way, but it worked gloriously with Swift 3 in Xcode 8.3.
Does anyone else have this problem?
I was able to get this to work in the latest Xcode by adding the following two lines to the top of the initialControlSetup function:
wantsLayer = true
canDrawSubviewsIntoLayer = true
I think this basically tells the NSView to render in a way that is more similar to how iOS works. If this worked in Xcode 8.3 as you say, it's possible that Apple introduced this regression in Xcode 9 without realizing it.
Dave's answer is correct, I just want to make a note on the consequences of this solution.
When canDrawSubviewsIntoLayer is set to true, all its sub views, that did not enable wantsLayer specifically, will render its contents using the layer of the parent view with canDrawSubviewsIntoLayer set to true.
This means sub view animations is disabled, since they lack a backing layer of their own. To prevent this from happening during runtime, you can put canDrawSubviewsIntoLayer = true into the prepareForInterfaceBuilder() function.
On a curious note, Interface Builder does not render the control, if you explicitly set button.wantsLayer = true, which according to the "canDrawSubviewsIntoLayer" documentation, should give the control its own backing layer and not render itself into the parent layer.
This is purely speculation, but I'm guessing as an optimisation, Interface Builder only renders the top layers/controls of the content view.

Make reusable component in Xcode storyboard

I have a specific situation but what I'm looking for is a generic solution. Currently I have a UIImageView that contains an image, a few labels, and multiple levels of constraints. I would like to configure this set of controls' properties once and reuse them inside of multiple controllers. Such that if I have to update this set, I would do it in one place and all the controller instances would get the change (sort of like how Sketch works with symbols).
You, sir, need a custom View!
My typical approach for this is to create an xib file, design the view I need, and create a class that subclasses UIView.
When you do this, you can assign the class of the xib File's Owner (in interface builder) and link up any #IBOutlets from the view to your custom class.
For the class, you'll need to implement a few methods. Here is an example custom view:
class LoadingView: UIView {
#IBOutlet var view: UIView!
#IBOutlet weak var messageLabel: UILabel!
#IBOutlet weak var activityIndicator: UIActivityIndicatorView!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
loadViewFromNib()
setUpView()
}
override init(frame: CGRect) {
super.init(frame: frame)
loadViewFromNib()
setUpView()
}
func setUpView() {
self.view.layer.cornerRadius = 10.0
self.view.layer.masksToBounds = true
}
private func loadViewFromNib() {
let bundle = Bundle.init(for: self.classForCoder)
bundle.loadNibNamed("LoadingView", owner: self, options: nil)
self.view.frame = bounds
self.addSubview(self.view)
}
}
You are required (pun intended) to implement the required init and the override init methods, and the other two are (kind of) optional. The loadViewFromNib is a convenience method that implements the logic to actually load the xib file from your app's bundle.
Don't forget to match the nib name with your xib file name! You'll thank me later. ;)
You can use this view in storyboards and use constraints, etc. by placing a regular old view and assigning its class to your custom class.
You can also play around with #IBDesignable to actually see your custom view in interface builder, though it tends to constantly reload and slow down Xcode unless you toggle a setting that I can't remember the name of right now (sorry!).
Enjoy!
What you want is not possible exactly in the way you describe it but there's a way to achieve the same result.
Create a subclass of UIView that will contain all the content you want, once you do that there are two options.
The first (and best, imo) option is to generate your layout with code when the view is initialized. This will allow you to add the view to other view controllers and it will initialize itself. The downside of this method is that you'll need to create the constraints with code.
The second option is to create a xib with your views and constraints and initialize your custom class from that xib. The downside of this is that you'll have to instantiate your view with code and place it in the view hierarchy yourself. You could create a container in the storyboard where you will add the view and pin it to the edges.