Best way to add multiple diagonal connection lines between TableViewCells - ios

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.

Related

A mystery about iOS autolayout with table views and self-sizing table view cells

To help in following this question, I've put up a GitHub repository:
https://github.com/mattneub/SelfSizingCells/tree/master
The goal is to get self-sizing cells in a table view, based on a custom view that draws its own text rather than a UILabel. I can do it, but it involves a weird layout kludge and I don't understand why it is needed. Something seems to be wrong with the timing, but then I don't understand why the same problem doesn't occur for a UILabel.
To demonstrate, I've divided the example into three scenes.
Scene 1: UILabel
In the first scene, each cell contains a UILabel pinned to all four sides of the content view. We ask for self-sizing cells and we get them. Looks great.
Scene 2: StringDrawer
In the second scene, the UILabel has been replaced by a custom view called StringDrawer that draws its own text. It is pinned to all four sides of the content view, just like the label was. We ask for self-sizing cells, but how will we get them?
To solve the problem, I've given StringDrawer an intrinsicContentSize based on the string it is displaying. Basically, we measure the string and return the resulting size. In particular, the height will be the minimal height that this view needs to have in order to display the string in full at this view's current width, and the cell is to be sized to that.
class StringDrawer: UIView {
#NSCopying var attributedText = NSAttributedString() {
didSet {
self.setNeedsDisplay()
self.invalidateIntrinsicContentSize()
}
}
override func draw(_ rect: CGRect) {
self.attributedText.draw(with: rect, options: [.truncatesLastVisibleLine, .usesLineFragmentOrigin], context: nil)
}
override var intrinsicContentSize: CGSize {
let measuredSize = self.attributedText.boundingRect(
with: CGSize(width:self.bounds.width, height:10000),
options: [.truncatesLastVisibleLine, .usesLineFragmentOrigin],
context: nil).size
return CGSize(width: UIView.noIntrinsicMetric, height: measuredSize.height.rounded(.up) + 5)
}
}
But something's wrong. In this scene, some of the initial cells have some extra white space at the bottom. Moreover, if you scroll those cells out of view and then back into view, they look correct. And all the other cells look fine. That proves that what I'm doing is correct, so why isn't it working for the initial cells?
Well, I've done some heavy logging, and I've discovered that at the time intrinsicContentSize is called initially for the visible cells, the StringDrawer does not yet correctly know its own final width, the width that it will have after autolayout. We are being called too soon. The width we are using is too narrow, so the height we are returning is too tall.
Scene 3: StringDrawer with workaround
In the third scene, I've added a workaround for the problem we discovered in the second scene. It works great! But it's horribly kludgy. Basically, in the view controller, I wait until the view hierarchy has been assembled, and then I force the table view to do another round of layout by calling beginUpdates and endUpdates.
var didInitialLayout = false
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
if !didInitialLayout {
didInitialLayout = true
UIView.performWithoutAnimation {
self.tableView.beginUpdates()
self.tableView.endUpdates()
}
}
}
The Mystery
Okay, so here are my questions:
(1) Is there a better, less kludgy workaround?
(2) Why do we need this workaround at all? In particular, why do we have this problem with my StringDrawer but not with a UILabel? Clearly, a UIlabel does know its own width early enough for it to give its own content size correctly on the first pass when it is interrogated by the layout system. Why is my StringDrawer different from that? Why does it need this extra layout pass?

Slide view together with UICollectionView cell

i have this structure. Scrollview -> UICollectionview + Label
This viewcontroller has array of items (BlockItem). On scrollViewDidScroll i change current test label (description)
func scrollViewDidScroll(_ scrollView:UIScrollView)
{
let midX:CGFloat = scrollView.bounds.midX
let midY:CGFloat = scrollView.bounds.midY
let point:CGPoint = CGPoint(x:midX, y:midY)
guard
let indexPath:IndexPath = collectionView.indexPathForItem(at:point)
else
{
return
}
let currentPage:Int = indexPath.item
if let found = items.first(where: {$0.id == String(block[currentPage])}) {
description.text = found.description
}
}
The main issue that i want my description label will be moving together when i move my collectionview cell and appear from the next cell. In other words, if I scroll to the left my description should move along with the cell, i.e. go to the left and appear on the right of the screen.
How can i do this? I know i could make a big collectionview but i need that only image part should be scrollable, not the entire block.
Here is what i want to achieve
Here is video example: scroll works only if i swipe on the image area, and doesn't work when i scroll down the page
https://drive.google.com/file/d/1kl1GYgXvK4bL3toTfOvpxF2WqS56pQO9/view?usp=sharing
You exactly said this:
"The main issue that I want my description label will be moving together when I move my collection view cell and appear from the next cell."
If you want your description label moving together, just include them into your collection cell. It's very clear from your point there
I don't understand why u need to separate it, but you want it to slide from new cell.
If you insisted that you want to archive the label, not in a scroll view, then use 2 label
One label for this cell and one label will come from scroll direction, u can archive this by creating manually. This is a simple code for a swipe left to scroll
UIView.animate(withDuration: 0.5) {
//insert your label animation
//current label moved with the scroll to left
//create a new label from the outer right of the current view, scroll it to the middle of your view
}
But it will be hard work for good animation or scroll support.

How to equally space labels in a UIStackView?

As a technical assessment for a job I'm interviewing for, I'm making a basic word search game, where the user looks for translations of a given word in a given language. I've got a fair amount of iOS experience, but I've never done dynamically-generated views with run-time-determined text labels, etc. before.
To be clear, I know this is a job assessment, but regardless of whether I get the job, or whether I'm even able to finish the assessment in time, I think this is an interesting problem and I'd like to learn how to do this, so I'll be finishing this as an app to run in the simulator or on my own phone.
So. I have a view embedded in/controlled by a UINavigationController. I have a couple of informational labels at the top, a set of buttons to perform actions across the bottom of the view, and the main view area needs to contain a 2D grid of characters. I'd like to be able to take an action when a character is tapped, such as highlight the character if it's part of a valid word. I'd like to be able to support grids of different sizes, so I can't just create an autolayout-constrained grid of labels or buttons in Interface Builder.
I've tried various methods for displaying the 2D grid of characters, but the one I thought had the most promise was as follows:
Use a UITableView to represent the rows of characters. Inside each table view cell, use a UIStackView with a dynamically generated collection of UILabels.
To fill my UITableView, I have the following:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = gameGrid.dequeueReusableCell(withIdentifier: "GameGridCell") as! TWSGameGridCell
if indexPath.row < word.grid.count {
cell.characters = word.grid[indexPath.row]
cell.configureCell()
return cell
}
return cell
}
The function in the custom table cell class that configures the cell (ideally to create and display the row of characters) is this:
func configureCell()
{
self.stackView = UIStackView(arrangedSubviews: [UILabel]())
self.stackView.backgroundColor = (UIApplication.shared.delegate as! TWSAppDelegate).appBackgroundColor()
self.stackView.distribution = .fillEqually
let myRect = self.frame
self.stackView.frame = myRect
let characterGridWidth = myRect.width / CGFloat(characters.count)
for cIndex in 0..<characters.count {
let labelRect = CGRect(x: myRect.origin.x + (CGFloat(cIndex) * characterGridWidth), y: myRect.origin.y, width: CGFloat(characterGridWidth), height: myRect.height)
let currentLabel = UILabel(frame: labelRect)
currentLabel.backgroundColor = (UIApplication.shared.delegate as! TWSAppDelegate).appBackgroundColor()
currentLabel.textColor = (UIApplication.shared.delegate as! TWSAppDelegate).appTextColor()
currentLabel.font = UIFont(name: "Palatino", size: 24)
currentLabel.text = characters[cIndex]
self.stackView.addArrangedSubview(currentLabel)
}
}
My guess is that it's running into trouble in that the labels have no visible rect when they're created, so they don't display anywhere.
The resulting view when run in the simulator is a white box covering as many rows of the table view as should be filled with rows of characters, and the rest of the table view shows with the custom background color I'm using), as per this image:
What I'm trying for is for that white box to have the same background color as everything else, and be filled with rows of characters. What am I missing?
Problem might be about adding 'labelRect', you already specify the rect of your stackView. I think regardless of the frame of labels, stackView should naturally be able to create it's inner labels and distribute them inside of itself.
Also can you add the following, after you initialize your stackView, inside your configureCell method:
self.stackView.axis = .horizontal
Edit: from the comment below, the solution was to create the labels (though I switched to buttons for further functionality), add them all to a [UIButton], then use that to create the UIStackView (self.stackView = UIStackView(arrangedSubviews: buttons)).

Different background and collectionCell separator color in UICollectionViewController

I have to create a UI which has 3 UICollectionViewCell in a row. So i created it using UICollectionViewController, with section = 1 and no of rows = coming from api. So i was able to create 6 cells in which every row contains 3 cells. These cells are separated by 0.5 pt vertically and horizontally.
I have given the background color to collectionView which comes in separator.
In particular this is what i need :-
Problem :-
This is the type of UICollectionViewController requirement i have
The separators needs to be red, and the space not rendered needs to be yellow. But I am unable to do that, I can have a single background color for UICollectionView which shows for both separator and free space. Is there a way to have different colors.
I dont want to use UITableView and have a UICollectionView inside UITableViewCell. I know this approach but i want to do the same in UICollectionViewController.
If I understand your question right, you can solve this by removing the space between the cells. Remove the 0.5pt space entirely. Then, all cells will be glued to each other, and you can create your own separators inside each cell. It'll require some logic though, because all cells should not use the same separators (if you do, then the center cells will have double separators on each side).
To achieve your photo, you could set these rules:
The first and the last cell in each row should only show a separator on bottom.
All the other cells in between (in your case, one cell in between) should show separator on bottom, left, and right.
To add these separators, just go into your custom UICollectionViewCell and add some UIViews to each side, and set them to have a width of 0.5pt, and set them visible on demand.
I would suggest a custom cell as Sti suggested. I usually do that. but an easier way would be:
To add a subview to the collectionView every time you reload data of
the collection view.
Code:
let count: CGFloat = CGFloat(items.count)
let numberOfRows: CGFloat = ceil(count / 3.0)
let height: CGFloat = numberOfRows * (cellHeight + seperatorHeight)
if collectionBackView != nil { // controller member object
collectionBackView.removeFromSuperview()
}
collectionBackView = UIView(frame:
CGRect(x: 0, y: 0, width: collectionView.frame.size.width, height: height))
collectionView.addSubview(collectionBackView)
collectionView.sendSubview(toBack: collectionBackView)

How to make UIView change it size as it scrolls

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.

Resources