Where to Set Column Width in NSTableView After Updates - notifications

I managed to write a function to set the column width by max string length. I need this to prevent long strings from clipping, wrapping etc.
My problem is now, where do I put the function. In other words when do I know when the tableview finished loading or updating? I cant find any available notifications for NSTableView.
here is my func:
func columnWidthByMaxStringLength(forColumn: Int) -> CGFloat {
let table = equationTableView!
var width = 0
for i in 0...table.numberOfRows-1 {
let view = table.view(atColumn: forColumn, row: i, makeIfNecessary: true)
let size = view?.fittingSize
width = max(width, Int((size?.width)!))
}
return CGFloat(width)
}
I tried to call this function within the NSTableView delegate func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? but it is not the right place. It throws an error.

The width of the column can be calculated when the table view and the data are loaded, before updating the table view. You can reuse the cell view.
For example:
override func viewDidLoad() {
super.viewDidLoad()
self.data = …
self.sizeToFit(column: tableView.column(withIdentifier: NSUserInterfaceItemIdentifier("name")))
self.tableView.reloadData()
}
func sizeToFit(column: Int) {
if let view = self.tableView.view(atColumn: column, row: 0, makeIfNecessary: true) as? NSTableCellView {
let key = self.tableView.tableColumns[column].identifier.rawValue
var width = self.tableView.tableColumns[column].minWidth
for object in self.data {
view.textField?.objectValue = object[key]
let size = view.fittingSize
width = max(width, size.width)
}
self.tableView.tableColumns[column].width = width
}
}

Related

How to display MKCompassButton with swiftui

I'm using swiftui and I would like to display the compass button.
The map portion of my code is derived from this tutorial:
https://www.morningswiftui.com/blog/build-mapview-app-with-swiftui
I've looked at sample code to display the compass on the map but I have not been able to find an example that I can get working with my swiftui code.
Actually compass is showing, but only when you try to turn your map. If you want to see compass button for all the time, you can add your own button in makeUIView func:
struct RootMapView: View {
var body: some View {
MapView()
}
}
struct MapView: UIViewRepresentable {
func makeUIView(context: Context) -> MKMapView {
let map = MKMapView()
map.showsCompass = false // hides current compass, which shows only on map turning
let compassBtn = MKCompassButton(mapView: map)
compassBtn.frame.origin = CGPoint(x: 20, y: 20) // you may use GeometryReader to replace it's position
compassBtn.compassVisibility = .visible // compass will always be on map
map.addSubview(compassBtn)
return map
}
func updateUIView(_ uiView: MKMapView, context: Context) {
}
}

Applying NSDiffableDataSourceSnapshot to UICollectionViewDiffableDataSource causes 'NSInternalInconsistencyException'

I am trying to implement a UICollectionViewDiffableDataSource for my collectionView. My code compiles fine, however I keep running into this error the first time I apply a snapshot to it, with the following error:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid parameter not satisfying: self.supplementaryViewProvider || (self.supplementaryReuseIdentifierProvider && self.supplementaryViewConfigurationHandler)'
Here is my code:
var groups: [Group] = [Group]()
var dataSource: UICollectionViewDiffableDataSource<Section, Group>!
// MARK: - View Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
self.searchBar.delegate = self
self.groups = DummyData.groups
setupDataSource()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
performSearch(searchQuery: nil)
}
// MARK: - Helper Functions
func performSearch(searchQuery: String?) {
let filteredGroups: [Group]
if let searchQuery = searchQuery, !searchQuery.isEmpty {
filteredGroups = groups.filter { $0.contains(query: searchQuery) }
} else {
filteredGroups = groups
}
var snapshot = NSDiffableDataSourceSnapshot<Section, Group>()
snapshot.appendSections([.main])
snapshot.appendItems(filteredGroups, toSection: .main)
dataSource.apply(snapshot, animatingDifferences: true, completion: nil)
}
func setupDataSource() {
dataSource = UICollectionViewDiffableDataSource <Section, Group>(collectionView: collectionView) { (collectionView: UICollectionView, indexPath: IndexPath, group: Group) -> UICollectionViewCell? in
guard let cell = self.collectionView.dequeueReusableCell(
withReuseIdentifier: String(describing: MyGroupsCollectionViewCell.self), for: indexPath) as? MyGroupsCollectionViewCell else {
fatalError("Cannot create new cell") }
cell.configure(withGroup: group)
return cell
}
}
If needed, I can post the full call stack.
Found the answer. I was using the storyboard to create my collectionView and accidentally had the attribute for Section Header set to true. Because of this, the collectionView needed to pull the view for the section header for somewhere, but I never told it where, hence the
parameter not satisfying: self.supplementaryViewProvider || (self.supplementaryReuseIdentifierProvider && self.supplementaryViewConfigurationHandler)
Here's a good article I found on it for anyone in the future who runs into this issue:
https://medium.com/#jamesrochabrun/uicollectionviewdiffabledatasource-and-decodable-step-by-step-6b727dd2485

Autosizing Cells in UICollectionView (all in code)

Using Swift-5.0, Xcode-10.2, iOS-12.2,
There is an issue in my code while trying to achieve "autosizing" of cells in a UICollectionView (using UICollectionViewFlowLayout as its layout).
The height of the cell is content-based (and unknown upfront) - therefore I try to get the cell's height to autosize (the "width" I don't care for now and can, for example, be set to frame.width).
Even tough, I only use one large UICollectionView-Cell for the example below, I would still like to keep "dequeueing" of the cells alive since later on, there will be many more cells that need to be filled with large content. Therefore, to make this example more simple, I keept the numberOfItemsInSection at 1.
Moreover, for the below example, each custom CollectionViewCell is filled with a vertical StackView (vertically adding-up a couple of Labels and ImageViews). The StackView is not important here, but I made it as a quick example. Again, the content of the custom CollectionViewCell will change later on (especially it will change to a content-dependent height). The fixed-height cell-content in the code below is just for example reasons...
But what I would still like to get out of this, is the question on how to make the CollectionViewCell autosize its height according to the content (whether fixed-height like in the below example or dynamic content-based-height like in the future implementation) ???
I keep getting the following Constraint-error:
Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one
you don't want.
Try this:
(1) look at each constraint and try to figure out which you don't expect;
(2) find the code that added the unwanted constraint or constraints and fix it.
Everyting works, if I give the Cell a very large fixed height using the method sizeForItemAt inside my UICollectionViewController. (i.e. with "very large" I mean much bigger than the cell's height turns out when dequeueing of the cell takes place).
But as explained above, I do not want to set the cell's height to a fixed value (using the UICollectionViewController's method sizeForItemAt - but rather achieve the desired autosizing.
To autosize the cell, I tried the following:
Approach A)
Use collectionViewFlowLayout.estimatedItemSize = CGSize(...)
(and don't use sizeForItemAt method)
--> Again same thing: If the estimated-size is set large enough, no error occurs. If I set it too small, then I get the same Constraint-error...
Approach B)
Use collectionViewFlowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
(and use or don't use sizeForItemAt method - does not make a difference)
--> Again same error: As soon as I start scrolling on the UICollectionView-cell, it throws the same Constraint-error...
Somewhat promising is the observation that using UICollectionViewFlowLayout.automaticSize makes the App work as desired (except that the above error is still thrown - but strange-enough, the App continues somehow to run anyway). For me it is not acceptable that the App works and an error is thrown. The question is, how to get rid of the Constraint-error ??
Here is all the code that I use:
class TestViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
let cellId = "cellID"
override func viewDidLoad() {
super.viewDidLoad()
collectionView.backgroundColor = .yellow
collectionView.register(MyCollectionViewCell.self, forCellWithReuseIdentifier: cellId)
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 1
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! MyCollectionViewCell
return cell
}
// func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
// return .init(width: view.frame.width, height: 4000)
// }
init() {
let collectionViewFlowLayout = UICollectionViewFlowLayout()
// collectionViewFlowLayout.estimatedItemSize = CGSize(width: UIScreen.main.bounds.width, height: 4000)
collectionViewFlowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
super.init(collectionViewLayout: collectionViewFlowLayout)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Here is the CustomCollectionViewCell:
import UIKit
class MyCollectionViewCell: UICollectionViewCell {
let titleLabel1 = UILabel(text: "Title 123", font: .boldSystemFont(ofSize: 30))
let titleLabel2 = UILabel(text: "Testing...", font: .boldSystemFont(ofSize: 15))
let imgView1 = UIImageView(image: nil)
let imgView2 = UIImageView(image: nil)
let imgView3 = UIImageView(image: nil)
let imgView4 = UIImageView(image: nil)
let imgView5 = UIImageView(image: nil)
let imageView: UIImageView = {
let imgView = UIImageView()
imgView.backgroundColor = .green
return imgView
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .yellow
titleLabel1.constrainHeight(constant: 50)
titleLabel1.constrainWidth(constant: 250)
titleLabel2.constrainHeight(constant: 50)
titleLabel2.constrainWidth(constant: 250)
imgView1.constrainHeight(constant: 100)
imgView1.constrainWidth(constant: 200)
imgView1.backgroundColor = .green
imgView2.constrainHeight(constant: 100)
imgView2.constrainWidth(constant: 200)
imgView2.backgroundColor = .green
imgView3.constrainHeight(constant: 100)
imgView3.constrainWidth(constant: 200)
imgView3.backgroundColor = .green
imgView4.constrainHeight(constant: 100)
imgView4.constrainWidth(constant: 200)
imgView4.backgroundColor = .green
imgView5.constrainHeight(constant: 100)
imgView5.constrainWidth(constant: 200)
imgView5.backgroundColor = .green
let stackView = UIStackView(arrangedSubviews: [titleLabel1, imgView1, titleLabel2, imgView2, imgView3, imgView4, imgView5])
addSubview(stackView)
stackView.anchor(top: safeAreaLayoutGuide.topAnchor, leading: leadingAnchor, bottom: bottomAnchor, trailing: trailingAnchor)
stackView.spacing = 20
stackView.axis = .vertical
stackView.alignment = .center
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Needed extensions are defined as such:
extension UILabel {
convenience init(text: String, font: UIFont) {
self.init(frame: .zero)
self.text = text
self.font = font
self.backgroundColor = .red
}
}
extension UIImageView {
convenience init(cornerRadius: CGFloat) {
self.init(image: nil)
self.layer.cornerRadius = cornerRadius
self.clipsToBounds = true
self.contentMode = .scaleAspectFill
}
}
To abstract away the anchoring and height-definitions of the autolayout-constraints of the cell's components (i.e. a bunch of labels and imageViews that fill the cell), I used the following UIView extension...:
(The extension has been published by Brian Voong - see video link)
// Reference Video: https://youtu.be/iqpAP7s3b-8
extension UIView {
#discardableResult
func anchor(top: NSLayoutYAxisAnchor?, leading: NSLayoutXAxisAnchor?, bottom: NSLayoutYAxisAnchor?, trailing: NSLayoutXAxisAnchor?, padding: UIEdgeInsets = .zero, size: CGSize = .zero) -> AnchoredConstraints {
translatesAutoresizingMaskIntoConstraints = false
var anchoredConstraints = AnchoredConstraints()
if let top = top {
anchoredConstraints.top = topAnchor.constraint(equalTo: top, constant: padding.top)
}
if let leading = leading {
anchoredConstraints.leading = leadingAnchor.constraint(equalTo: leading, constant: padding.left)
}
if let bottom = bottom {
anchoredConstraints.bottom = bottomAnchor.constraint(equalTo: bottom, constant: -padding.bottom)
}
if let trailing = trailing {
anchoredConstraints.trailing = trailingAnchor.constraint(equalTo: trailing, constant: -padding.right)
}
if size.width != 0 {
anchoredConstraints.width = widthAnchor.constraint(equalToConstant: size.width)
}
if size.height != 0 {
anchoredConstraints.height = heightAnchor.constraint(equalToConstant: size.height)
}
[anchoredConstraints.top, anchoredConstraints.leading, anchoredConstraints.bottom, anchoredConstraints.trailing, anchoredConstraints.width, anchoredConstraints.height].forEach{ $0?.isActive = true }
return anchoredConstraints
}
func fillSuperview(padding: UIEdgeInsets = .zero) {
translatesAutoresizingMaskIntoConstraints = false
if let superviewTopAnchor = superview?.topAnchor {
topAnchor.constraint(equalTo: superviewTopAnchor, constant: padding.top).isActive = true
}
if let superviewBottomAnchor = superview?.bottomAnchor {
bottomAnchor.constraint(equalTo: superviewBottomAnchor, constant: -padding.bottom).isActive = true
}
if let superviewLeadingAnchor = superview?.leadingAnchor {
leadingAnchor.constraint(equalTo: superviewLeadingAnchor, constant: padding.left).isActive = true
}
if let superviewTrailingAnchor = superview?.trailingAnchor {
trailingAnchor.constraint(equalTo: superviewTrailingAnchor, constant: -padding.right).isActive = true
}
}
func centerInSuperview(size: CGSize = .zero) {
translatesAutoresizingMaskIntoConstraints = false
if let superviewCenterXAnchor = superview?.centerXAnchor {
centerXAnchor.constraint(equalTo: superviewCenterXAnchor).isActive = true
}
if let superviewCenterYAnchor = superview?.centerYAnchor {
centerYAnchor.constraint(equalTo: superviewCenterYAnchor).isActive = true
}
if size.width != 0 {
widthAnchor.constraint(equalToConstant: size.width).isActive = true
}
if size.height != 0 {
heightAnchor.constraint(equalToConstant: size.height).isActive = true
}
}
func centerXInSuperview() {
translatesAutoresizingMaskIntoConstraints = false
if let superViewCenterXAnchor = superview?.centerXAnchor {
centerXAnchor.constraint(equalTo: superViewCenterXAnchor).isActive = true
}
}
func centerYInSuperview() {
translatesAutoresizingMaskIntoConstraints = false
if let centerY = superview?.centerYAnchor {
centerYAnchor.constraint(equalTo: centerY).isActive = true
}
}
func constrainWidth(constant: CGFloat) {
translatesAutoresizingMaskIntoConstraints = false
widthAnchor.constraint(equalToConstant: constant).isActive = true
}
func constrainHeight(constant: CGFloat) {
translatesAutoresizingMaskIntoConstraints = false
heightAnchor.constraint(equalToConstant: constant).isActive = true
}
}
Finally, after some more digging, I've found a solution:
Four things are important if you want to autosize your custom UICollectionViewCell:
when creating your UICollectionViewController instance, make sure you pass a FlowLayout having set the estimatedItemSize property to some educated-guess value (in my case with a height = 1 [since unknown])
let collectionViewFlowLayout = UICollectionViewFlowLayout()
collectionViewFlowLayout.estimatedItemSize = CGSize(width: UIScreen.main.bounds.width, height: 1)
let vc = TestViewController(collectionViewLayout: collectionViewFlowLayout)
.
create a property inside your custom UICollectionViewCell that keeps track of the cell's height
(i.e. inside your UICollectionViewCell class, you will always know what the cell's height is (even if dynamically changed later on). Again, I wanted to get rid of the fact that I needed to define this height already at the UICollectionViewController's sizeForItemAt method before dequeueing the cell)
Don't forget to add the preferredLayoutAttributesFitting method inside your custom UICollectionViewCell :
let myContentHeight = CGFloat(720)
// in my above code-example, the 720 are the sum of 2 x 50 (labels) plus 5 x 100 (imageViews) plus 6 x 20 (spacing)
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
setNeedsLayout()
layoutIfNeeded()
var newFrame = layoutAttributes.frame
// myContentHeight corresponds to the total height of your custom UICollectionViewCell
newFrame.size.height = ceil(myContentHeight)
layoutAttributes.frame = newFrame
return layoutAttributes
}
Of course, whenever your custom CollectionViewCell changes its height dynamically, you also need to change the myContentHeight property again...
(the calling of preferredLayoutAttributesFitting you don't need to care yourself in code, this method will be called automatically whenever the user enters the view, scrolls or does anything else. The OS takes more or less care of this...).

Moving object to location of touch

How can I move the 'player' position to be where the user touches the screen? I want them to be able to move it constantly by tapping different places. This is what I have so far but it's not working:
class GameScene: SKScene {
let player = SKSpriteNode(imageNamed: "Spaceship")
override func didMoveToView(view: SKView) {
// 2
backgroundColor = SKColor.whiteColor()
// 3
player.position = CGPoint(x: size.width/2, y: size.height/2)
// 4
addChild(player)
}
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
/* Called when a touch begins */
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
let player = SKSpriteNode(imageNamed:"Spaceship")
player.position = location
}
}
In the line let player = SKSpriteNode(imageNamed:"Spaceship") in touchesBegan you are creating a new instance of the spaceship node.
This node is not in the scene and setting the position of the new node does not change the position of the existing player node.
Just try resetting the position of you player node instead.
Try
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
let touch = touches.anyObject() as UITouch?
if let location = touch?.locationInNode(self)
{
player.position = location
}
}

Swift unique property in multiple instances

I know that theoretically it's possible to create multiple instances of the same class with a property that would have a different value for each instance.
The thing is, I can't make it happen.
Each time I'm creating a new instance, it gets the property's value of the other instances, and when I'm changing one value for an instance, it changes the other's too.
So my guess is that I'm doing something wrong (obviously), like accessing the class property value instead of the instance property value... Here's the code.
class CustomUIImageView: UIImageView {
var someParameter: Bool = false // This is the property I want to be different in each version of the instance.
}
class ClassSiege: UIViewController, UIGestureRecognizerDelegate {
var myView: CustomUIImageView! //the instance declaration.
// I use this gesture recognizer to find out the value of the instance I'm tapping on.
func handleTap (sender: UITapGestureRecognizer) {
print("value of someParameter \(self.myView.someParameter)")
}
func handlePan(recognizer: UIPanGestureRecognizer) {
let iv: UIView! = recognizer.view
let translation = recognizer.translationInView(self.view)
iv.center.x += translation.x
iv.center.y += translation.y
recognizer.setTranslation(CGPointZero, inView: self.view)
var centerBoardX = BlackBoard.center.x // 'Blackboard' is a fixed image on the screen.
var centerBoardY = BlackBoard.center.y
var centerRondX = iv.center.x
var centerRondY = iv.center.y
if centerRondY - centerBoardY < 100 {
self.myView.someParameter = true // If the distance between myView and the blackboard is under 100 I want the instance's property to become true.
} else {
self.myView.someParameter = false // On the other hand, if the distance is greater than 100, I want it to be false.
}
}
// When the user pushes a button, it triggers this method that creates a new instance of myView and add it to the screen.
#IBAction func showContent(sender: AnyObject) {
// some code...
// Here I'm creating the instance of the view and I give it the gesture recognizer parameters. I don't think that relevant to the issue, so I'm not adding the code.
}
}
So clearly that's not the good way to do it, but what's wrong, and how can it be solved?
Basing my answer on your related question.
If what you want to achieve is initializing a property with a value that you provide, just add a new parameter to the initializer. If for instance you are using the initializer with a CGRect passed in, then you can implement an initializer like this:
class CustomUIImageView : UIImageView {
let someParameter : Bool
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
init(frame: CGRect, someParameter: Bool) {
self.someParameter = someParameter
super.init(frame: frame)
}
}
I hope that this is what you are looking for - let me know otherwise.
I've found the solution, and if you've been facing the same issu, here's how to deal with it.
The secret is to downcast the recognizer.view to take the parameter of the subclass CustomUIImageView.
here's how :
func handleTap (sender: UITapGestureRecognizer) {
println("value of someParameter \(self.myView.someParameter)") //I use this gesture recognizer to find out the value of the instance I'm tapping on.
}
func handlePan(recognizer:UIPanGestureRecognizer) {
let iv : UIView! = recognizer.view
let translation = recognizer.translationInView(self.view)
iv.center.x += translation.x
iv.center.y += translation.y
recognizer.setTranslation(CGPointZero, inView: self.view)
var centerBoardX = BlackBoard.center.x //blackboard is a fixed image on the screen.
var centerBoardY = BlackBoard.center.y
var centerRondX = iv.center.x
var centerRondY = iv.center.y
var myParameter = recognizer.view as CustomUIImageView //<- this is the key point. Downcasting let you access the custom subclass parameters of the object that is currently moved
if centerRondY - centerBoardY < 100 {
myParameter.someParameter = true //so now I'm really changing the parameter's value inside the object rather than changing a global var like I did before.
} else {
myParameter.someParameter = false
}
}
//when user pushes a button, it triggers this func that creates a new instance of myView and add it to the screen.
#IBAction func showContent(sender: AnyObject) {
some code...
//here I'm creating the instance of the view and I give it the gesture recognizer parameters. I don't think that relevant to the issue, so I'm not adding the code.
}