I have an NSCollectionView which consists of dozens of rows of single-columned items (chat messages) in a messaging application.
Each item contains a text area of which the heights vary. Therefore the view should be defaulted to the bottom when the view is created and scrolled to the bottom when new messages are received.
I am struggling to either default the scroll to the bottom or work out how to get the height of the CollectionView's contents to scroll to the bottom.
Any help would be greatly appreciated, thanks!
If you support only macOS 10.11 and up, and if you use a NSCollectionViewLayout, you could ask the layouter for the size of the entire content. That's also how the scrollbars are getting sized.
So, in ObjC, you'd simply ask your collectionView for its layouter's content size and scroll to the bottom:
NSSize contentSize = theCollectionView.collectionViewLayout.collectionViewContentSize.height;
[theCollectionView.enclosingScrollView.contentView scrollPoint:NSMakePoint(0, contentSize.height)];
hi i suggest you to work on Offset for horizontal UICollectionView and Apple provided a method for bottom for vertical UICollectionView
UICollectionView Horizontal Next and Previous Item
extension UICollectionView {
func scrollToNextItem() {
let contentOffset = CGFloat(floor(self.contentOffset.x + self.bounds.size.width))
self.moveToFrame(contentOffset: contentOffset)
}
func scrollToPreviousItem() {
let contentOffset = CGFloat(floor(self.contentOffset.x - self.bounds.size.width))
self.moveToFrame(contentOffset: contentOffset)
}
func moveToFrame(contentOffset : CGFloat) {
let frame: CGRect = CGRect(x: contentOffset, y: self.contentOffset.y , width: self.frame.width, height: self.frame.height)
self.scrollRectToVisible(frame, animated: true)
}
}
UICollectionView Vertical Scroll to Bottom
extension UICollectionView {
func scrollToBottom(animated: Bool) {
let sections = self.numberOfSections
if sections > 0 {
let rows = self.numberOfItems(inSection: sections - 1)
let last = IndexPath(row: rows - 1, section: sections - 1)
DispatchQueue.main.async {
self.scrollToItem(at: last, at: .bottom, animated: animated)
}
}
}
}
UITableView Scroll to Bottom
extension UITableView {
func scrollToBottom(animated: Bool) {
let sections = self.numberOfSections
if sections > 0 {
let rows = self.numberOfRows(inSection: sections - 1)
let last = IndexPath(row: rows - 1, section: sections - 1)
DispatchQueue.main.async {
self.scrollToRow(at: last, at: .bottom, animated: animated)
}
}
}
}
Usage
self.collectionView.scrollToBottom(animated: true)
self.tableView.scrollToBottom(animated: true)
Related
I have a UITableViewCell containing a simple UIStackView and 2 UILabels (So it is from UIKit, NOT native SwiftUI) that should have a static width and dynamic height. How can I have a preview for this without need to see the actual phone size?
Note 1: .sizeThatFits will put all the weight on the width and there will be no multiline labels
Note 2: .device is showing extra useless empty spaces of the main view of the screen.
Note 3: .fixed(width:height:) is not prefered, because it will have less space or extra useless space as our needs.
Note 4: Need something like this: (For UIStackView)
DEMO
struct UILabelPorted: UIViewRepresentable {
var configuration = { (view: UILabel) in }
func makeUIView(context: UIViewRepresentableContext<Self>) -> UILabel {
let uiView = UIViewType()
uiView.setContentHuggingPriority(.defaultHigh, for: .vertical)
uiView.setContentHuggingPriority(.defaultHigh, for: .horizontal)
return uiView
}
func updateUIView(_ uiView: UILabel, context: UIViewRepresentableContext<Self>) { configuration(uiView) }
}
struct UILabel_Previews: PreviewProvider {
static var previews: some View {
UILabelPorted {
$0.text = "This label should have multiple lines like it is in a UITableView cell."
}
.previewLayout(.sizeThatFits)
}
}
It is not very clear where is the problem, but you need something like
ContentView()
.previewLayout(.fixed(width: 414, height: 300))
I have tableview cells with an image view and a label.
The images are downloaded from a URL. They can be landscape or portrait which will determine their height constraint later on.
The label can be multiple lines and its lines property is set to 0, and the imageview is set to Aspect fill, in the attributes inspector.
I want to vertically resize the cells to fit the image and the label.
In cellForRow if I set the imageview's height constraint to something small like 100 then the image view covers the label. If I set it to 300 then it partially covers the label. And if I set it to something really high like 10000 it stretches the image but the label is not covered at all.
Here is the code:
class ArtistListViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 250
}
}
extension ArtistListViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! ArtistTableViewCell
cell.imageHeightConstraint.constant = 100
let artist = artists[indexPath.row]
cell.descriptionLabel.text = artist.description
setupCellForImage(cell, artist: artist)
return cell
}
fileprivate func setupCellForImage(_ cell: ArtistTableViewCell, artist: Artist) {
let url = URL(string: artist.image)
URLSession.shared.dataTask(with: url!, completionHandler: { (data, response, error) in
DispatchQueue.main.async(execute: {
cell.artistImageView.image = UIImage(data: data!)
})
}).resume()
}
}
ImageView constraints:
Top space to superview = 0
Bottom space to description label = 20
Height = 235
Label constraints:
Bottom space to superview = 8
I have not changed priority of content hugging or compression resistance.
Here are 2 images where the height constraint is set to 300. While the images are loading and once finished loading:
What is going on here? Why is the cell not resizing according to the image view's height?
In ImageView, Attribute Inspector you need to select Clip To Bounds . Then it will solve one part of your problem. Then you need to setup constraint correctly.
I've got this extension for my collection view that makes horizontal scroll but I want to change it on vertical scroll??
extension viewRe : UIScrollViewDelegate
{
func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>)
{
let layout = self.recipesCollView?.collectionViewLayout as! UICollectionViewFlowLayout
let cellWidthIncludingSpacing = layout.itemSize.width + layout.minimumLineSpacing
var offset = targetContentOffset.memory
let index = (offset.x + scrollView.contentInset.left) / cellWidthIncludingSpacing
let roundedIndex = round(index)
offset = CGPoint(x: roundedIndex * cellWidthIncludingSpacing - scrollView.contentInset.left, y: -scrollView.contentInset.top)
targetContentOffset.memory = offset
}
}
With this extension I'm trying to make the cells to stick to the TOP of the view even when he scrolls because the paging is enabled. So whenever the user scrolls i would like the cell to stick to the top and so on when the user scrolls.
#donnyWals is right. If you are using a UICollectionView just change its UICollectionViewFlowLayout
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = . Horizontal
let collectionView = UICollectionView(frame: frame, collectionViewLayout: layout)
or if you have an existent UICollectionView
if let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
layout.scrollDirection = .Horizontal
}
Follow the official API:
UICollectionView
UICollectionViewFlowLayout
I want to implement a feature that when an user hovers over the specific area, the new view appears with drawer-like animation. And also, when the user leaves the specific area, the drawer should go away with animation. This is exactly what you see when you hover over the bottom of the screen in OS X, where the Dock appears and disappears with animation.
However, if I implement the feature with animation, it does not work properly when you re-enter the specific area before the animation in the mouseExited: is completed. Here's my code:
let trackingArea = NSTrackingArea(rect: CGRectMake(0, 0, 120, 300), options: NSTrackingAreaOptions.ActiveAlways | NSTrackingAreaOptions.MouseEnteredAndExited, owner: self, userInfo: nil)
underView.addTrackingArea(trackingArea) // underView is the dummy view just to respond to the mouse tracking, since the drawerView's frame is changed during the animation; not sure if this is the clean way...
override func mouseEntered(theEvent: NSEvent) {
let frameAfterVisible = CGRectMake(0, 0, 120, 300)
NSAnimationContext.runAnimationGroup({
(context: NSAnimationContext!) in
context.duration = 0.6
self.drawerView.animator().frame = frameAfterVisible
}, completionHandler: { () -> Void in
})
}
override func mouseExited(theEvent: NSEvent) {
let frameAfterInvisible = CGRectMake(-120, 0, 120, 300)
NSAnimationContext.runAnimationGroup({
(context: NSAnimationContext!) in
context.duration = 0.6
self.drawerView.animator().frame = frameAfterInvisible
}, completionHandler: { () -> Void in
})
}
// drawerView's frame upon launch is (-120, 0, 120, 300), since it is not visible at first
In this code, I animate the drawerView by altering its x position. However, as I stated, when you enter the tracking area and then leave the tracking area, the drawer works correctly. But that is not the case if you re-enter the tracking area before the leave-off animation is fully completed.
Of course if I set the animation duration shorter, such as 0.1, this would rarely occur. But I want to move the view with animation.
What I want to do is make the drawerView start to appear again even if the view has not completed disappearing. Is there any practice to do it?
I have a solution that is very similar to your code. What I do different is is that I install the NSTrackingArea not on the view that contains the drawer view, but on the drawer view itself.
This obviously means that the drawer needs to 'stick out' a little bit. In my case the drawer is a small bit visible when it is down because I put an image view in it. If you don't want that then I suggest you just leave the visible area of the drawer empty and translucent.
Here is my implementation:
private enum DrawerPosition {
case Up, Down
}
private let DrawerHeightWhenDown: CGFloat = 16
private let DrawerAnimationDuration: NSTimeInterval = 0.75
class ViewController: NSViewController {
#IBOutlet weak var drawerView: NSImageView!
override func viewDidLoad() {
super.viewDidLoad()
// Remove the auto-constraints for the image view otherwise we are not able to change its position
view.removeConstraints(view.constraints)
drawerView.frame = frameForDrawerAtPosition(.Down)
let trackingArea = NSTrackingArea(rect: drawerView.bounds,
options: NSTrackingAreaOptions.ActiveInKeyWindow|NSTrackingAreaOptions.MouseEnteredAndExited,
owner: self, userInfo: nil)
drawerView.addTrackingArea(trackingArea)
}
private func frameForDrawerAtPosition(position: DrawerPosition) -> NSRect {
var frame = drawerView.frame
switch position {
case .Up:
frame.origin.y = 0
break
case .Down:
frame.origin.y = (-frame.size.height) + DrawerHeightWhenDown
break
}
return frame
}
override func mouseEntered(event: NSEvent) {
NSAnimationContext.runAnimationGroup({ (context: NSAnimationContext!) in
context.duration = DrawerAnimationDuration
self.drawerView.animator().frame = self.frameForDrawerAtPosition(.Up)
}, completionHandler: nil)
}
override func mouseExited(theEvent: NSEvent) {
NSAnimationContext.runAnimationGroup({ (context: NSAnimationContext!) in
context.duration = DrawerAnimationDuration
self.drawerView.animator().frame = self.frameForDrawerAtPosition(.Down)
}, completionHandler: nil)
}
}
Full project at https://github.com/st3fan/StackOverflow-28777670-TrackingArea
Let me know if this was useful. Happy to make changes.
Starting Swift 3. You need to do it like this:
let trackingArea = NSTrackingArea(rect: CGRectMake(0, 0, 120, 300), options: [NSTrackingAreaOptions.ActiveAlways ,NSTrackingAreaOptions.MouseEnteredAndExited], owner: self, userInfo: nil)
view.addTrackingArea(trackingArea)
Credits #marc above!
I am working on Xcode 6.1.1 on OSX 10.10. I am trying out storyboards for Mac apps. I have a NSTabViewController using the new NSTabViewControllerTabStyleToolbar tabStyle and it is set as the default view controller for the window controller. How do I make my window resize according to the current selected view controller?
Is it possible to do entirely in Interface Builder?
Here is what my storyboard looks like:
The auto layout answer is half of it. You need to set the preferredContentSize in your ViewController for each tab to the fitting size (if you wanted the tab to size to the smallest size satisfying all constraints).
override func viewWillAppear() {
super.viewWillAppear()
preferredContentSize = view.fittingSize
}
If your constraints are causing an issue below try first with a fixed size, the example below sets this in the tab item's view controller's viewWillAppear function (Swift used here, but the Objective-C version works just as well).
override func viewWillAppear() {
super.viewWillAppear()
preferredContentSize = NSSize(width: 400, height: 280)
}
If that works, fiddle with your constraints to figure out what's going on
This solution for 'toolbar style' tab view controllers does animate and supports the nice crossfade effect. In the storyboard designer, add 'TabViewController' in the custom class name field of the NSTabViewController. Don't forget to assign a title to each viewController, this is used as a key value.
import Cocoa
class TabViewController: NSTabViewController {
private lazy var tabViewSizes: [String : NSSize] = [:]
override func viewDidLoad() {
// Add size of first tab to tabViewSizes
if let viewController = self.tabViewItems.first?.viewController, let title = viewController.title {
tabViewSizes[title] = viewController.view.frame.size
}
super.viewDidLoad()
}
override func transition(from fromViewController: NSViewController, to toViewController: NSViewController, options: NSViewController.TransitionOptions, completionHandler completion: (() -> Void)?) {
NSAnimationContext.runAnimationGroup({ context in
context.duration = 0.5
self.updateWindowFrameAnimated(viewController: toViewController)
super.transition(from: fromViewController, to: toViewController, options: [.crossfade, .allowUserInteraction], completionHandler: completion)
}, completionHandler: nil)
}
func updateWindowFrameAnimated(viewController: NSViewController) {
guard let title = viewController.title, let window = view.window else {
return
}
let contentSize: NSSize
if tabViewSizes.keys.contains(title) {
contentSize = tabViewSizes[title]!
}
else {
contentSize = viewController.view.frame.size
tabViewSizes[title] = contentSize
}
let newWindowSize = window.frameRect(forContentRect: NSRect(origin: NSPoint.zero, size: contentSize)).size
var frame = window.frame
frame.origin.y += frame.height
frame.origin.y -= newWindowSize.height
frame.size = newWindowSize
window.animator().setFrame(frame, display: false)
}
}
The window containing a toolbar style tab view controller does resize without any code if you have auto layout constraints in your storyboard tab views (macOS 11.1, Xcode 12.3). I haven't tried other style tab view controllers.
If you want to resize with animation as in Finder, it is sufficient to add one override in your tab view controller. It will resize the window with system-calculated resize animation time and will hide the tab view during resize animation:
class PreferencesTabViewController: NSTabViewController {
override func transition(from fromViewController: NSViewController, to toViewController: NSViewController, options: NSViewController.TransitionOptions = [], completionHandler completion: (() -> Void)? = nil) {
guard let window = view.window else {
super.transition(from: fromViewController, to: toViewController, options: options, completionHandler: completion)
return
}
let fromSize = window.frame.size
let toSize = window.frameRect(forContentRect: toViewController.view.frame).size
let widthDelta = toSize.width - fromSize.width
let heightDelta = toSize.height - fromSize.height
var toOrigin = window.frame.origin
toOrigin.x += widthDelta / 2
toOrigin.y -= heightDelta
let toFrame = NSRect(origin: toOrigin, size: toSize)
NSAnimationContext.runAnimationGroup { context in
context.duration = window.animationResizeTime(toFrame)
view.isHidden = true
window.animator().setFrame(toFrame, display: false)
super.transition(from: fromViewController, to: toViewController, options: options, completionHandler: completion)
} completionHandler: { [weak self] in
self?.view.isHidden = false
}
}
}
Please adjust closure syntax if you are using Swift versions older than 5.3.
Use autolayout. Set explicit size constraints on you views. Or once you have entered the UI into each tab view item's view set up the internal constraints such that they force view to be the size you want.