NSSplitviewController programmatically - macos

I have a NSSplitViewController with 2 view controllers as splitViewItems.
This works fine.
Now I would like to set an own SplitViewController Class for my Splitviewcontroller in the storyboard. I give it my own class start the app. But now I only see an empty window.
Have I set my splitviewcontroller now programmatically, if I set my own splitviewcontroller class?
If yes, which code I have to use to show the two view controllers in my splitview controller again?
UPDATE
import Cocoa
class SplitViewController: NSSplitViewController {
override func viewDidLoad() {
print("Test")
}
}

Here is an Xcode 9 Playground (Swift 4) which shows how to setup NSSplitViewController from code.
import Cocoa
import PlaygroundSupport
class ViewController: NSViewController {
private let backgroundColor: NSColor
init(backgroundColor: NSColor) {
self.backgroundColor = backgroundColor
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
view = NSView()
view.wantsLayer = true
view.layer?.backgroundColor = backgroundColor.cgColor
}
}
class MainSplitViewController: NSSplitViewController {
private let splitViewResorationIdentifier = "com.company.restorationId:mainSplitViewController"
lazy var vcA = ViewController(backgroundColor: .red)
lazy var vcB = ViewController(backgroundColor: .green)
lazy var vcC = ViewController(backgroundColor: .blue)
override init(nibName nibNameOrNil: NSNib.Name?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
setupUI()
setupLayout()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
}
extension MainSplitViewController {
private func setupUI() {
view.wantsLayer = true
splitView.dividerStyle = .paneSplitter
splitView.autosaveName = NSSplitView.AutosaveName(rawValue: splitViewResorationIdentifier)
splitView.identifier = NSUserInterfaceItemIdentifier(rawValue: splitViewResorationIdentifier)
}
private func setupLayout() {
minimumThicknessForInlineSidebars = 180
let itemA = NSSplitViewItem(sidebarWithViewController: vcA)
itemA.minimumThickness = 80
addSplitViewItem(itemA)
let itemB = NSSplitViewItem(contentListWithViewController: vcB)
itemB.minimumThickness = 100
addSplitViewItem(itemB)
let itemC = NSSplitViewItem(viewController: vcC)
itemC.minimumThickness = 80
addSplitViewItem(itemC)
}
}
let vc = MainSplitViewController()
vc.view.frame = CGRect(x: 0, y: 0, width: 400, height: 300)
PlaygroundPage.current.liveView = vc
UPDATE:
Version similar to one above but targeted to macOS 10.10.
import Cocoa
import PlaygroundSupport
class ViewController: NSViewController {
private let backgroundColor: NSColor
init(backgroundColor: NSColor) {
self.backgroundColor = backgroundColor
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
view = NSView()
view.wantsLayer = true
view.layer?.backgroundColor = backgroundColor.cgColor
}
}
class MainSplitViewController: NSSplitViewController {
private let splitViewResorationIdentifier = "com.company.restorationId:mainSplitViewController"
lazy var vcA = ViewController(backgroundColor: .red)
lazy var vcB = ViewController(backgroundColor: .green)
lazy var vcC = ViewController(backgroundColor: .blue)
override init(nibName nibNameOrNil: NSNib.Name?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
setupUI()
setupLayout()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
}
extension MainSplitViewController {
private func setupUI() {
view.wantsLayer = true
splitView.dividerStyle = .paneSplitter
splitView.autosaveName = NSSplitView.AutosaveName(rawValue: splitViewResorationIdentifier)
splitView.identifier = NSUserInterfaceItemIdentifier(rawValue: splitViewResorationIdentifier)
vcA.view.widthAnchor.constraint(greaterThanOrEqualToConstant: 80).isActive = true
vcB.view.widthAnchor.constraint(greaterThanOrEqualToConstant: 100).isActive = true
vcC.view.widthAnchor.constraint(greaterThanOrEqualToConstant: 80).isActive = true
}
private func setupLayout() {
let sidebarItem = NSSplitViewItem(viewController: vcA)
sidebarItem.canCollapse = true
sidebarItem.holdingPriority = NSLayoutConstraint.Priority(NSLayoutConstraint.Priority.defaultLow.rawValue + 1)
addSplitViewItem(sidebarItem)
let xibItem = NSSplitViewItem(viewController: vcB)
addSplitViewItem(xibItem)
let codeItem = NSSplitViewItem(viewController: vcC)
addSplitViewItem(codeItem)
}
}
let vc = MainSplitViewController()
vc.view.frame = CGRect(x: 0, y: 0, width: 400, height: 300)
PlaygroundPage.current.liveView = vc

Related

Getting currently focused NSTextView in SwiftUI from WindowGroup's CommandGround-item

Question
I've implemented a NSTextView SwiftUI-wrapper (following this great example). There are several of these NSTextViews on my view. In the app's menu, there is a button that should change the currently focused NSTextView's content, e.g:
Is there a way to determine which NSTextView is currently focused? In my current solution, I resorted to storing the NSTextView in a global view model's variable by passing the NSTextView when its "becomeFirstResponder" is called.
However, I'm afraid this solution could either lead to retain cycles or to the NSTextView stored in the view model becoming nil. Is there a cleaner way of doing this? Any help is appreciated!
Current solution/Code
NSTextView
struct TextArea: NSViewRepresentable {
// Source : https://stackoverflow.com/a/63761738/2624880
#Binding var text: NSAttributedString
#Binding var selectedRange: NSRange
#Binding var isFirstResponder: Bool
func makeNSView(context: Context) -> NSScrollView {
context.coordinator.createTextViewStack()
}
func updateNSView(_ nsView: NSScrollView, context: Context) {
if let textArea = nsView.documentView as? NSTextView {
textArea.textStorage?.setAttributedString(self.text)
if !(self.selectedRange.location == textArea.selectedRange().location && self.selectedRange.length == textArea.selectedRange().length) {
textArea.setSelectedRange(self.selectedRange)
}
// Set focus (SwiftUI 👉 AppKit)
if isFirstResponder {
nsView.becomeFirstResponder()
DispatchQueue.main.async {
if ViewModel.shared.focusedTextView != textArea {
ViewModel.shared.focusedTextView = textArea
}
}
} else {
nsView.resignFirstResponder()
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(text: $text, selectedRange: $selectedRange, isFirstResponder: $isFirstResponder)
}
class Coordinator: NSObject, NSTextViewDelegate {
var text: Binding<NSAttributedString>
var selectedRange: Binding<NSRange>
var isFirstResponder: Binding<Bool>
init(text: Binding<NSAttributedString>,
selectedRange: Binding<NSRange>,
isFirstResponder: Binding<Bool>) {
self.text = text
self.selectedRange = selectedRange
self.isFirstResponder = isFirstResponder
}
func textView(_ textView: NSTextView, shouldChangeTextIn range: NSRange, replacementNSAttributedString text: NSAttributedString?) -> Bool {
defer {
self.text.wrappedValue = textView.attributedString()
self.selectedRange.wrappedValue = textView.selectedRange()
}
return true
}
fileprivate lazy var textStorage = NSTextStorage()
fileprivate lazy var layoutManager = NSLayoutManager()
fileprivate lazy var textContainer = NSTextContainer()
fileprivate lazy var textView: NSTextViewWithFocusHandler = NSTextViewWithFocusHandler(frame: CGRect(), textContainer: textContainer)
fileprivate lazy var scrollview = NSScrollView()
func textDidChange(_ notification: Notification) {
guard let textView = notification.object as? NSTextView else { return }
self.text.wrappedValue = NSAttributedString(attributedString: textView.attributedString())
self.selectedRange.wrappedValue = textView.selectedRange()
}
func textViewDidChangeSelection(_ notification: Notification) {
guard let textView = notification.object as? NSTextView else { return }
DispatchQueue.main.async {
if !(self.selectedRange.wrappedValue.location == textView.selectedRange().location && self.selectedRange.wrappedValue.length == textView.selectedRange().length) {
self.selectedRange.wrappedValue = textView.selectedRange()
}
}
}
func textDidBeginEditing(_ notification: Notification) {
DispatchQueue.main.async {
self.isFirstResponder.wrappedValue = true
}
}
func textDidEndEditing(_ notification: Notification) {
DispatchQueue.main.async {
self.isFirstResponder.wrappedValue = false
}
}
func createTextViewStack() -> NSScrollView {
let contentSize = scrollview.contentSize
textContainer.containerSize = CGSize(width: contentSize.width, height: CGFloat.greatestFiniteMagnitude)
textContainer.widthTracksTextView = true
textView.minSize = CGSize(width: 0, height: 0)
textView.maxSize = CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
textView.isVerticallyResizable = true
textView.frame = CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height)
textView.autoresizingMask = [.width]
textView.delegate = self
scrollview.borderType = .noBorder
scrollview.hasVerticalScroller = true
scrollview.documentView = textView
scrollview.layer?.cornerRadius = 10
scrollview.drawsBackground = false
textStorage.addLayoutManager(layoutManager)
layoutManager.addTextContainer(textContainer)
return scrollview
}
}
}
class NSTextViewWithFocusHandler: NSTextView {
override func becomeFirstResponder() -> Bool {
// ⚠️ Set self as currently focused TextView (AppKit 👉 SwiftUI)
ViewModel.shared.focusedTextView = self
return super.becomeFirstResponder()
}
}
ViewModel
class ViewModel: ObservableObject {
static let shared = ViewModel()
#Published var attributedTextQuestion: NSAttributedString = NSAttributedString(string: "Initial value question")
#Published var attributedTextAnswer: NSAttributedString = NSAttributedString(string: "Initial value answer")
#Published var selectedRangeQuestion: NSRange = NSRange()
#Published var selectedRangeAnswer: NSRange = NSRange()
#Published var questionFocused: Bool = false // Only works in direction SwiftUI 👉 AppKit
#Published var answerFocused: Bool = false // (dito)
weak var focusedTextView: NSTextView? {didSet{
DispatchQueue.main.async {
self.menuItemEnabled = self.focusedTextView != nil
}
}}
#Published var menuItemEnabled: Bool = false
}
ContentView
struct ContentView: View {
#ObservedObject var model: ViewModel
var body: some View {
VStack {
Text("Question")
TextArea(text: $model.attributedTextQuestion,
selectedRange: $model.selectedRangeQuestion,
isFirstResponder: $model.questionFocused)
Text("Answer")
TextArea(text: $model.attributedTextAnswer,
selectedRange: $model.selectedRangeAnswer,
isFirstResponder: $model.answerFocused)
}
.padding()
}
}
App
#main
struct TextViewMacOSSOFrageApp: App {
#ObservedObject var model: ViewModel = ViewModel.shared
var body: some Scene {
WindowGroup {
ContentView(model: model)
}.commands {
CommandGroup(replacing: .textFormatting) {
Button(action: {
// ⚠️ The currently focused TextView is retrieved and its AttributedString updated
guard let focusedTextView = model.focusedTextView else { return }
let newAttString = NSMutableAttributedString(string: "Value set through menu item")
newAttString.addAttribute(.backgroundColor, value: NSColor.yellow, range: NSRange(location: 0, length: 3))
focusedTextView.textStorage?.setAttributedString(newAttString)
focusedTextView.didChangeText()
}) {
Text("Insert image")
}.disabled(!model.menuItemEnabled)
}
}
}
}

Xcode: How do I reset the state of all Buttons from the main view controller using a class

I have a class for my buttons that changes the colour when pressed, alternating between on and off.
class KSPickButton: UIButton {
var isOn = true
override init(frame: CGRect) {
super.init(frame: frame)
initButton()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initButton()
}
func initButton() {
layer.borderWidth = 2.0
layer.borderColor = Colors.shanklinGreen.cgColor
layer.cornerRadius = frame.size.height/2
backgroundColor = .clear
setTitleColor(.white, for: .normal)
addTarget(self, action: #selector(KSPickButton.buttonPressed), for: .touchUpInside)
}
#objc func buttonPressed() {
activateButton(bool: !isOn)
}
func activateButton(bool: Bool) {
isOn = bool
let color = bool ? .clear : Colors.shanklinGreen
//let title = bool ? "" : ""
let titleColor = bool ? .white: Colors.shanklinBlack
//setTitle(title, for: .normal)
setTitleColor(titleColor, for: .normal)
backgroundColor = color
}
}
This works perfectly. I have 20 buttons on my main view controller and they flip between on and off as expected... Then maybe after pressing 6, I want to reset them all to off. I have a reset button on my main view controller, but I cannot work out how I can reset them all?
I can make them all look reset but the bool remains as was...
How do I call this class for all buttons and reset them correctly?
Introduced an observer with 'isOn' variable. You can try using following code snippet in 'KSPickButton' class. After that you need to take all subviews from view controllers (where buttons are placed) and set 'isOn' to 'false' for all of them.
class KSPickButton: UIButton {
public var isOn:Bool = true {
didSet {
handleButtonStateChange()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
initButton()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initButton()
}
func initButton() {
layer.borderWidth = 2.0
layer.borderColor = Colors.shanklinGreen.cgColor
layer.cornerRadius = frame.size.height/2
backgroundColor = .clear
setTitleColor(.white, for: .normal)
addTarget(self, action: #selector(KSPickButton.buttonPressed), for: .touchUpInside)
}
#objc func buttonPressed() {
isOn = !isOn
}
func handleButtonStateChange() {
let color = isOn ? .clear : Colors.shanklinGreen
let titleColor = isOn ? .white: Colors.shanklinBlack
setTitleColor(titleColor, for: .normal)
backgroundColor = color
}
}
Here is code snippet you need to implement in your view controller as explained above.
class MyVC: UIViewController {
------
------
for subview in view.subviews where subview.isKind(of: KSPickButton.self)&&(subview as? KSPickButton)?.isOn == true {
(subview as? KSPickButton)?.isOn = false
}
-----
-----
}

NSCollectionView single selection is not working, but multiple selection is fine

As title described,
my NSCollectionView is not working when single selection.
MyNSCollectionView is rendered correcttly,
below code shows how I initialize my NSCollectionView:
self.leftBar.dataSource = self
self.leftBar.delegate = self
self.leftBar.isSelectable = true
self.leftBar.allowsEmptySelection = true
let layout = NSCollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.itemSize = NSSize(width: 200 , height: 50)
leftBar.collectionViewLayout = layout
self.contentView.addSubview(leftBar)
leftBar <- [
Top(),
Left(),
Bottom(),
Width(200)
]
Custom NSCollectionViewItem
class LeftBarCell: NSCollectionViewItem {
var leftBarView : LeftBarView?
override func loadView() {
leftBarView = LeftBarView(frame: NSZeroRect)
view = leftBarView!
}
func setup(title : String){
leftBarView?.titleTextView.string = title
}
}
In LeftBarView
class LeftBarView: NSView {
lazy var titleTextView : NSTextView = {
let titleTextView = NSTextView()
titleTextView.isEditable = false
titleTextView.isSelectable = false
titleTextView.font = NSFont(name: "Helvetica", size: 20)
return titleTextView
}()
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
self.setupViews()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
func setupViews(){
self.addSubview(titleTextView)
self.wantsLayer = true
self.layer?.backgroundColor = NSColor.lightGray.cgColor
titleTextView <- Edges()
}
}
I tried to play around with
self.leftBar.isSelectable = true
leftBar.allowsMultipleSelection = true
and multiple selection is working.
The didSelectItemsAt from NSCollectionViewDelegate is triggered
But then when I try with this
self.leftBar.isSelectable = true
or
self.leftBar.isSelectable = true
self.leftBar.allowsEmptySelection = true
It is not working when I click on the collectionviewitem,
The didSelectItemsAt from NSCollectionViewDelegate is not triggered.
Any thoughts would be appreciated, thanks!
Solved. This is because the textview covers the cell, u might need to disable the textview to become passive.
extension NSTextView {
open override func hitTest(_ point: NSPoint) -> NSView? {
return nil
}
}

Image annotation different

I created a map view with different annotation and a search. I can not attribute a different image according to the pin. I also have another concern, when I do a search it removes a pin to put it on the new place sought. Below is the code of a young coder. In advance thank you for your help.
import MapKit
import UIKit
import CoreLocation
class MapViewController: UIViewController, UISearchBarDelegate, MKMapViewDelegate {
#IBOutlet weak var MapView: MKMapView!
var searchController:UISearchController!
var annotation:MKAnnotation!
var localSearchRequest:MKLocalSearchRequest!
var localSearch:MKLocalSearch!
var localSearchResponse:MKLocalSearchResponse!
var error:NSError!
var pointAnnotation:MKPointAnnotation!
var pinAnnotationView:MKPinAnnotationView!
var coordinates: [[Double]]!
var name:[String]!
let regionRadius: CLLocationDistance = 1000
#IBAction func showSearchBar(_ sender: Any) {
searchController = UISearchController(searchResultsController: nil)
searchController.hidesNavigationBarDuringPresentation = false
self.searchController.searchBar.delegate = self
present(searchController, animated: true, completion: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
self.MapView.delegate = self
var Tirta = CustomPointAnnotation()
Tirta.coordinate = CLLocationCoordinate2DMake(-8.415162, 115.315360)
Tirta.title = "Tirta Empul"
Tirta.imageName = "PaConseil.png"
var Goa = CustomPointAnnotation()
Goa.coordinate = CLLocationCoordinate2DMake(-8.551313, 115.468865)
Goa.title = "Goa Lawah"
Goa.imageName = "PaMecontent.png"
MapView.addAnnotation(Tirta)
MapView.addAnnotation(Goa)
// 3
let region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: -8.670458199999999, longitude: 115.2126293), span: MKCoordinateSpan(latitudeDelta: 2, longitudeDelta: 2))
self.MapView.setRegion(region, animated: true)
}
func MapView(mapView: MKMapView!, viewForAnnotation annotation: MKAnnotation!) -> MKAnnotationView! {
print("delegate called")
if !(annotation is CustomPointAnnotation) {
return nil
}
let reuseId = "test"
var AnnotationView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseId)
if AnnotationView == nil {
AnnotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
AnnotationView?.canShowCallout = true
}
else {
AnnotationView?.annotation = annotation
}
func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
let CustomPointAnnotation = annotation as! CustomPointAnnotation
AnnotationView?.image = UIImage(named:CustomPointAnnotation.imageName)
return AnnotationView
}
class CustomPointAnnotation: MKPointAnnotation {
var imageName: String!
}
func searchBarSearchButtonClicked(_ searchBar: UISearchBar){
//1
searchBar.resignFirstResponder()
dismiss(animated: true, completion: nil)
if self.MapView.annotations.count != 0{
annotation = self.MapView.annotations[0]
self.MapView.removeAnnotation(annotation)
}
//2
localSearchRequest = MKLocalSearchRequest()
localSearchRequest.naturalLanguageQuery = searchBar.text
localSearch = MKLocalSearch(request: localSearchRequest)
localSearch.start { (localSearchResponse, error) -> Void in
if localSearchResponse == nil{
let alertController = UIAlertController(title: nil, message: "Place Not Found", preferredStyle: UIAlertControllerStyle.alert)
alertController.addAction(UIAlertAction(title: "Dismiss", style: UIAlertActionStyle.default, handler: nil))
self.present(alertController, animated: true, completion: nil)
return
}
//3
self.pointAnnotation = MKPointAnnotation()
self.pointAnnotation.title = searchBar.text
self.pointAnnotation.coordinate = CLLocationCoordinate2D(latitude: localSearchResponse!.boundingRegion.center.latitude, longitude: localSearchResponse!.boundingRegion.center.longitude)
self.pinAnnotationView = MKPinAnnotationView(annotation: self.pointAnnotation, reuseIdentifier: nil)
self.MapView.centerCoordinate = self.pointAnnotation.coordinate
self.MapView.addAnnotation(self.pinAnnotationView.annotation!)
}
}
}
import Foundation
import MapKit
class CustomPointAnnotation: MKPointAnnotation {
var coordinates: CLLocationCoordinate2D
var name: String!
var imageName: UIImage!
init(coordinates: CLLocationCoordinate2D) {
self.coordinates = coordinates
}
}
I'm going to assume that you've updated Xcode so that you are now using Swift 3. Well then, the problem is that this method is never being executed:
func MapView(mapView: MKMapView!,
viewForAnnotation annotation: MKAnnotation!) -> MKAnnotationView! {
... and the reason is that that is not its correct "signature". That method is now defined like this:
func mapView(_ mapView: MKMapView,
viewFor annotation: MKAnnotation) -> MKAnnotationView?

Swift - GLKit View CIFilter Image

I am trying to use a GLIKit View in order to modify an Image. The class I have so far is working well all the CIFilters except for the CILineOverlay it renders a black view. If I use any other effect it works well.
Why is the CILineOverlay not showing?
class ImageView: GLKView {
let clampFilter = CIFilter(name: "CIAffineClamp")!
let blurFilter = CIFilter(name: "CILineOverlay")!
let ciContext:CIContext
override init(frame: CGRect) {
let glContext = EAGLContext(API: .OpenGLES2)
ciContext = CIContext(
EAGLContext: glContext,
options: [
kCIContextWorkingColorSpace: NSNull()
]
)
super.init(frame: frame, context: glContext)
enableSetNeedsDisplay = true
}
required init(coder aDecoder: NSCoder) {
let glContext = EAGLContext(API: .OpenGLES2)
ciContext = CIContext(
EAGLContext: glContext,
options: [
kCIContextWorkingColorSpace: NSNull()
]
)
super.init(coder: aDecoder)!
context = glContext
enableSetNeedsDisplay = true
}
#IBInspectable var inputImage: UIImage? {
didSet {
inputCIImage = inputImage.map { CIImage(image: $0)! }
}
}
#IBInspectable var blurRadius: Float = 0 {
didSet {
//blurFilter.setValue(blurRadius, forKey: "inputIntensity")
setNeedsDisplay()
}
}
var inputCIImage: CIImage? {
didSet { setNeedsDisplay() }
}
override func drawRect(rect: CGRect) {
if let inputCIImage = inputCIImage {
clampFilter.setValue(inputCIImage, forKey: kCIInputImageKey)
blurFilter.setValue(clampFilter.outputImage!, forKey: kCIInputImageKey)
let rect = CGRect(x: 0, y: 0, width: drawableWidth, height: drawableHeight)
ciContext.drawImage(blurFilter.outputImage!, inRect: rect, fromRect: inputCIImage.extent)
}
}
}
The Apple docs states "The portions of the image that are not outlined are transparent." - this means you are drawing black lines over a black background. You can simply composite the output from the filter over a white background to make the lines appear:
let background = CIImage(color: CIColor(color: UIColor.whiteColor()))
.imageByCroppingToRect(inputCIImage.extent)
let finalImage = filter.outputImage!
.imageByCompositingOverImage(background)

Resources