In the view controller class I'm trying to make buttons programmatically in a 10x10 grid. I have constraints and the view is resizing properly, but the size I'm getting back and seeing from the print statement is the original size from the story board and not the resized size. How do I get the new size?
func buttonGridder() {
for x in 0..<10 {
for y in 0..<10{
let sizer = ButtonGrid.frame.width
let buttonSize:CGFloat = ButtonGrid.frame.width / CGFloat(10)
print("\(x), \(y), \(buttonSize), \(sizer)")
let letterButton = WordButton(column: x, row: y, buttonSize: buttonSize, buttonMargin: 0)
self.ButtonGrid.addSubview(letterButton)
}
}
}
Call the Log function in this method viewDidLayoutSubviews. This is the method that is called after views are positioned accordingly to the constraints after viewDidAppear. Note : This method is called multiple times in a single go, so DO NOT alloc or add in this method.
AND if you are adding or changing the view size later on then call setNeedsDisplay to redraw all the Views inside the View controller
Related
I could not find much detail about how to add a customView as decoration for UICalenderView. There are many blogs telling how to add images but could not find anyone about CustomView. In images, we can return the decoration item with size parameter however in case of customView there is no option to pass size along with customView that you are adding. So in the end, I was able to add a view with red background, but the size is wrong. I tried to create a view and give it frame but it had no effect. So im confused how to adjust its size. Here is the method in which I add customView that im creating:
func calendarView(_ calendarView: UICalendarView, decorationFor dateComponents: DateComponents) -> UICalendarView.Decoration? {
return .customView(addActivityCircle)
}
And this is my addActivityCircle method which for now just creating a view with red background color:
private func addActivityCircle() -> UIView {
let view = UIView()
view.backgroundColor = .red
view.clipsToBounds = false
view.frame = CGRect(x: 0, y: 0, width: 50, height: 50)
return view
}
When I run this code I do see a view with red color but it's like a small rectangle, not 50x50. If I pass small values like 20x20, I do see a small rectangle but anything above that I see a rectangle of fixed size. I think that's the limit of decoration item but in apps like Fitness app by apple, there are bigger activity rings than that so there should be a way to have bigger sized custom views as this is just too small. The width is fine but the height is just too less. This is what im getting and it does not get any higher than that:
I have a bit complex UICollectionView with my custom UICollectionViewLayout.
I added a pinch gesture behavior onto it and adjust the layout according to that result.
It's basically working pretty well, the cells are resized and repositioned properly in normal cases.
But when it comes to a particular condition the cells disappear and never appear again.
So far I'm unable to clarify the condition, but it happens often when you pinched the view to a smaller size.
The issue is, layoutAttributesForElements in my collection view layout is called (of course this also implies that numberOfCells:inSection is called as well) and it's returning reasonable cell geometry, but actual cell generation (cellForItemAt:) won't be called.
Before jumping into the code(as it's a bit too long and complicated) I want to ask you guys if any of you have had the same kind of situation.
Below is the summary of what's happening and what I see so far.
It's not that always happening. It happens only after pinching and reached to a certain condition. So this is not a basic kind of how-to-use-UICollectionView issue.
Even when it happens layoutAttributesForElements keeps being called (as you keep pinching)
The layout attributes don't have crazy values like zero size or position of far out of view range. They all have the position attributes that fit into the collection view's content size.
Collection view claims the proper content view size (at least as reported on the debugger.)
When it happens you cannot see any cells in the view hierarchy in the View Debugger. Meaning, it's not zero sized or clear colored but cells themselves are gone. This corresponds to the fact that cellForItemAt: is not called.
After it happened you cannot see the cells anymore even you pinch the screen back to the original scale.
Any information is appreciated. Thanks!
EDIT
My collection view layout is like this. This project is a musical sequencer and the collection view is showing your musical notes in piano roll style.
//
// YMPianoRollLayout.swift
//
import UIKit
class YMPianoRollLayout: UICollectionViewLayout {
let notes : Array<CGPoint> = []
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!;
}
override var collectionViewContentSize : CGSize {
let cv = self.collectionView as! YMPianoRollCollectionView
let screenInfo = cv.pianoRollViewController.screenInfo
let totalSize = screenInfo.collectionViewContentsSize();
print("contentSize = \(totalSize)")
return totalSize;
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
// "screenInfo" keeps the user specified view parameters including the scale ratio by the pinch gesture
let cv = self.collectionView as! YMPianoRollCollectionView;
let pianoRoll = cv.pianoRollViewController;
// Check which musical note can be included in the view rect
let indexArray: Array<Int> = pianoRoll!.getNoteIndexes(inRect:rect, useOnlyStartTime: false);
var retArray : [UICollectionViewLayoutAttributes] = []
for i in indexArray {
if let _ = pianoRoll?.pattern.eventSequence[i] as? YMPatternEventNoteOn {
retArray.append( self.layoutAttributesForPatternEventInfo(i) )
}
}
// This always reports non-zero count. Also checked the positions of each array members
// by breaking here and they all had proper size and positions
print("retArray count = \(retArray.count)");
return retArray
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let i = Int((indexPath as NSIndexPath).row)
return self.layoutAttributesForPatternEventInfo(i)
}
//
// This is my specific func to convert the musical time-pitch into the view geometory
//
func layoutAttributesForPatternEventInfo(_ index: Int) -> UICollectionViewLayoutAttributes!{
let cv = self.collectionView as! YMPianoRollCollectionView
// "screenInfo" keeps the user specified view parameters including the scale ratio by the pinch gesture
let screenInfo = cv.pianoRollViewController.screenInfo
// Retrieve musical event
let event = cv.pianoRollViewController.pattern.eventSequence[index]
let index = IndexPath(row:index, section: 0)
let newAttr = UICollectionViewLayoutAttributes(forCellWith:index)
var frame : CGRect!
if let e = event as? YMPatternEventNoteOn {
let noteNo = e.noteNo;
let origin = YMMusicalValuePoint(time:event.time, noteNumber:noteNo);
let size = YMMusicalValueSize(timeLength:e.duration, numberOfNotes: 1);
// Actual size calculation is done in my "screenInfo" class.
frame = screenInfo.getPhysicalRange(YMMusicalValueRange(origin:origin, size:size));
} else {
frame = CGRect(x: 0, y: 0, width: 0, height: 0);
}
newAttr.frame = frame;
newAttr.zIndex = 1;
return newAttr
}
//
// For checking the bounds
//
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
print("BBB newBounds = \(newBounds)")
return true
}
}
Sounds like an interesting problem...
More info on your custom UICollectionViewLayout subclass (i.e. the code) would be useful but I can offer an overview of how UICollectionView typically updates visible content in response to bounds changes (i.e. content offset, rotation) and invalidateLayout calls.
When invalidateLayout fires the internal attributes cache is cleared signaling the need to ask the layout on the next layoutSubviews pass what should be visible. Conversely, if the layout is not invalidated and the attributes are known in the cache the layout will not be asked.
The magic all happens when the current CA transaction commits and UICollectionView's layoutSubviews is invoked. At this point, a diff between what was last known to be on screen and what should now be on screen is computed according the current "visible bounds" (effectively the scroll view's bounds which includes the offset into the layout).
Cells no longer visible in the new visible bounds will be returned to the reuse queue and newly appearing cells (as per the layout) will be constructed and existing items (that have changed) will be updated with their new attributes.
Based on your description, it sounds like when the next layoutSubviews fires the queried attributes (possibly from the cache!) aren't returning anything for the new visible bounds therefore nothing "new" appears -- along with the existing items disappearing...
A few things to investigate might include:
Is your gesture causing your custom layout to invalidate? If not, it probably should so the UICollectionView knows to not trust it's internal attributes cache and always ask the layout for new attributes during the "diffing" process.
How is the bounds of the collection changing during the pinch gesture?
This directly affects the diff since it will use this visible bounds to determine what should be displayed next.
How does your layout respond to shouldInvalidateForBoundsChange:?
Most layouts only invalidate when the extents (e.g. rotation) change, so the UICollectionView will normally rely on it's cached attributes when performing the diff. In your case if you are tweaking the bounds during the gesture, you'll want to unconditionally return YES here.
When you get into this wonky state, you might try pausing into the debugger e.g.
po [[cv collectionViewLayout] invalidateLayout]
po [cv setNeedsLayout]
po [cv layoutIfNeeded]
...and resume. Has everything re-appeared?
If so, it sounds like the layout isn't being invalidated under certain circumstances and the attributes being returned are indeed reasonable.
If not, ping the layout and see what if it is reporting as visible is reasonable via:
po [[cv collectionViewLayout] layoutAttributesForElementsInRect:cv.bounds]
I'm creating an app that needs to show a tableview like below image
Similar colored circles are to be matched with a line.
Which view i can add the lines?
Or need to create a new view above tableview? But still my tableview needs to be scrolled.
How can i achieve this?
Update for Bounty
I want to implement the same with incliend lines between neighbouring circles. How to achieve the same?
Demonstration below:
create design like this
Based on your requirement just hide upper line and lower line of circle
You need to create collection view in tableview cell. In collection view you create one cell. Design the same user interface like your design. Show and hide the view with matching of rule. It will not affect tableview scrolling. and with this approach you can also provide scroll in collection view cell. i can provide you coded solution if you able to provide me more information. Thanks
You can use this Third Party LIb
You need to use a combination of collection view and a table view to give support for all devices.
1.Create one collection view cell with following layout
Hide upper and lower lines as per your need
Add collection view in table view cell and managed a number of cells in collection view depending upon the current device width and item's in between spacing.
You can create a vertical label without text, set the background color with black and place it behind the circle in view hierarchy and set a width of the label as per your requirement. Then you can hide unhide the label whenever you want.
P.S.: Make sure to hide your cell separator.
I have created a demo project. You can find it here. I tried to match your requirements. You can update the collection view settings to handle the hide and show of labels.
Let me know if you have any questions.
Hope this help.
Thanks!
To connect any circle with any other circle in the cell above / below, it will be easier and cleaner to create the connection lines dynamically rather than building them into the asset as before. This part is simple. The question now is where to add them.
You could have the connection lines between every two cells be contained in the top or bottom cell of each pair, since views can show content beyond their bounds.
There's a problem with this though, regardless of which cell contains the lines. For example, if the top cell contains them, then as soon as it is scrolled up off screen, the lines will disappear when didEndDisplayingCell is called, even though the bottom cell is still completely on screen. And then scrolling slightly such that cellForRow is called, the lines will suddenly appear again.
If you want to avoid that problem, then here is one approach:
One Approach
Give your table view and cells a clear background color, and have another table view underneath to display a new cell which will contain the connection lines.
So you now have a background TVC, with a back cell, and a foreground TVC with a fore cell. You add these TVC's as children in a parent view controller (of which you can set whatever background color you like), disable user interaction on the background TVC, and peg the background TVC's content offset to the foreground TVC's content offset in an observation block, so they will stay in sync when scrolling. I've done this before; it works well. Use the same row height, and give the background TVC a top inset of half the row height.
We can make the connection lines in the back cell hug the top and bottom edges of the cell. This way circles will be connected at their centre.
Perhaps define a method in your model that calculates what connections there are, and returns them, making that a model concern.
extension Array where Element == MyModel {
/**
A connection is a (Int, Int).
(0, 0) means the 0th circle in element i is connected to the 0th circle in element j
For each pair of elements i, j, there is an array of such connections, called a mesh.
Returns n - 1 meshes.
*/
func getMeshes() -> [[(Int, Int)]] {
// Your code here
}
}
Then in your parent VC, do something like this:
class Parent_VC: UIViewController {
var observation: NSKeyValueObservation!
var b: Background_TVC!
override func viewDidLoad() {
super.viewDidLoad()
let b = Background_TVC(model.getMeshes())
let f = Foreground_TVC(model)
for each in [b, f] {
self.addChild(each)
each.view.frame = self.view.bounds
self.view.addSubview(each.view)
each.didMove(toParent: self)
}
let insets = UIEdgeInsets(top: b.tableView.rowHeight / 2, left: 0, bottom: 0, right: 0)
b.tableView.isUserInteractionEnabled = false
b.tableView.contentInset = insets
self.b = b
self.observation = f.tableView.observe(\.contentOffset, options: [.new]) { (_, change) in
let y = change.newValue!.y
self.b.tableView.contentOffset.y = y // + or - half the row height
}
}
}
Then of course there's your drawing code. You could make it a method of your back cell class (a custom cell), which will take in a mesh data structure and then draw the lines that represent it. Something like this:
class Back_Cell: UITableViewCell {
/**
Returns an image with all the connection lines drawn for the given mesh.
*/
func createMeshImage(for mesh: [(Int, Int)]) -> UIImage {
let canvasSize = self.contentView.bounds.size
// Create a new canvas
UIGraphicsBeginImageContextWithOptions(canvasSize, false, 0)
// Grab that canvas
let canvas = UIGraphicsGetCurrentContext()!
let spacing: CGFloat = 10.0 // whatever the spacing between your circles is
// Draw the lines
for each in mesh {
canvas.move(to: CGPoint(x: CGFloat(each.0) * spacing, y: 0))
canvas.addLine(to: CGPoint(x: CGFloat(each.1) * spacing, y: self.contentView.bounds.height))
}
canvas.setStrokeColor(UIColor.black.cgColor)
canvas.setLineWidth(3)
canvas.strokePath()
let image = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return image
}
}
You'd probably want to create a Mesh class and store the images in that model, to avoid redrawing.
I am implementing a scroll view picker I have implemented most of the part but I'm stuck at changing the size when it scrolls.
I'm trying to do this :
I have got till here and i don't know how to change size using scroll offset i tried many things but failed i need help
I'm using SwipeView to get the scroll
func swipeViewDidScroll(swipeView: SwipeView!) {
//USE swipeView.currentItemView to access current view
//USE swipeView.itemViewAtIndex(swipeView.currentItemIndex - 1) to access previous view
//USE swipeView.itemViewAtIndex(swipeView.currentItemIndex + 1) to access next View
swipeView.currentItemView.frame = CGRect(x: swipeView.currentItemView.bounds.origin.x, y: swipeView.currentItemView.bounds.origin.y , width: (swipeView.currentItemView.bounds.width + swipeView.scrollOffset) - CGFloat((280 * swipeView.currentItemIndex)), height: swipeView.currentItemView.bounds.height)
}
I have attached the project to make your work easier understanding my work
LINK TO PROJECT
What about this:
create a special class for the cells in the slider
when the cell is created, add NSLayoutContraints of width and aspect ratio to the view
create a method to expand the cell using the constraint inside the class, like
-(void)expandCell {
self.widthConstraint.constant = normalWidth * 1.05f;
}
-(void)normalizeCell {
self.widthConstraint.constant = normalWidth;
}
when the view is at the center you call the method expandCell to expand it 5% and when the cell is out you call the normalizeCell. To make things prettier you just animate that.
I have a label in a View Controller scene inside a root view.
I set the constraint of the height to 30 using the Pins button right below.
Everything looks pretty in the storyboard.
But when I want to get the size of the element inside my code I always get the value 21. I tried refLabel.bounds.size.height and refLabel.frame.height. I call this in the override func viewDidLoad(){
super.viewDidLoad() function of the corresponding class.
Has this something to do with the intrinsic content size?
Try this code
refLabel.layoutIfNeeded()
let height = refLabel.frame.size.height
print("right height is \(height)")