Count of Array (within an Array) to determin UIView width - ios

I am trying to figure out how to get the count of an array within an array (do not know the technical term).
I am creating groups of UIViews, and I would like the width of these groups to change dynamically depending on the number of UIViews that are within the group.
The only way I know how to do this (potentially) is to find the count of by blockTitles (group), but I am having issues pulling this count from within my palletViewConstraint() function.
Looking for help with this solution or recommendations for better solutions.
Cheers!
func mainViews(inputView: UIView){
<...>
//POPULATING PALLET WITH BLOCKS
let blockTitles1 = ["1", "2", "3", "4", "5"]
let blockTitles2 = ["6", "7", "8", "9", "10"]
let blockTitles3 = ["11", "12", "13", "14", "15"]
let blockTitles4 = ["16", "17", "18", "19", "20"]
let blockTitles5 = ["21", "22", "23", "24", "25"]
var group1 = createBlockGroup(blockTitles1)
var group2 = createBlockGroup(blockTitles2)
var group3 = createBlockGroup(blockTitles3)
var group4 = createBlockGroup(blockTitles4)
var group5 = createBlockGroup(blockTitles5)
palletView.addSubview(group1)
palletView.addSubview(group2)
palletView.addSubview(group3)
palletView.addSubview(group4)
palletView.addSubview(group5)
palletViewConstraint(palletView, groups: [group1, group2, group3, group4, group5], blockTitles: [blockTitles1, blockTitles2, blockTitles3, blockTitles4, blockTitles5])
}
func createBlock(title: String) -> UIView{
let block = UIView()
block.backgroundColor = UIColor.lightGrayColor()
block.translatesAutoresizingMaskIntoConstraints = false
let blockLabel = UILabel()
blockLabel.frame = CGRectMake(0, 0, 100, 100)
blockLabel.text = title
blockLabel.textColor = UIColor.darkGrayColor()
blockLabel.textAlignment = NSTextAlignment.Center
block.addSubview(blockLabel)
return block
}
func createBlockGroup(blockTitles: [String]) -> UIView {
var blocks = [UIView]()
let blockGroups = UIView()
blockGroups.frame = CGRectMake(0, 0, 100, 100)
blockGroups.backgroundColor = UIColor.redColor()
for blockTitle in blockTitles{
let block = createBlock(blockTitle)
blocks.append(block)
blockGroups.addSubview(block)
}
blockConstraint(blocks, mainView: blockGroups)
return blockGroups
}
func blockConstraint(blocks: [UIView], mainView: UIView){
<...>
}
func palletViewConstraint(inputView: UIView, groups: [UIView], blockTitles: [[String]]){
print(blockTitles.count)
}
func mainViewConstraints(inputView: UIView, pallet: UIScrollView, canvas: UIView){
<...>
}
}

You can access the count of an array within an array by using
mainArr[0].count
This will get the count of the first array in the mainArr.
I would recommend subclassing UIView and using a convenience initializer to set the frame to a value divided by the number of views (which can be accessed by a delegate pattern).
Here is an example of UIView subclass that I have set up to dynamically adjust a grid of subviews. The whole project can be found on my github page: https://github.com/cmako10/UI-View-Groups
Here is the subclass:
class GroupView: UIView {
var rows: Int!
var columns: Int!
var size: CGSize!
var origin: CGPoint!
enum TypeToAdd: String {
case row = "Row"
case column = "Column"
}
enum AlterAction: String {
case add = "Add"
case delete = "Delete"
}
convenience init(origin: CGPoint, size: CGSize, columns: Int, rows: Int) {
self.init(frame: CGRect(origin: origin, size: size))
self.size = size
self.origin = origin
self.rows = rows
self.columns = columns
}
override func drawRect(rect: CGRect) {
frame.size.width = size.width
frame.size.height = size.height
super.drawRect(rect)
for view in self.subviews {
view.removeFromSuperview()
}
let width = size.width / CGFloat(columns)
let height = size.height / CGFloat(rows)
let subviewSize = CGSize(width: width, height: height)
for y in 0..<rows {
for x in 0..<columns {
let newView = UIView(frame: CGRect(origin: CGPoint(x: (CGFloat(x) * width), y: (CGFloat(y) * height)), size: subviewSize))
let label = UILabel(frame: CGRect(origin: CGPoint(x: 0, y: 0), size: subviewSize))
label.adjustsFontSizeToFitWidth = true
label.textAlignment = .Center
label.font = UIFont.systemFontOfSize(15, weight: 10.0)
label.textColor = UIColor.blackColor()
label.center = CGPoint(x: newView.frame.midX, y: newView.frame.midY)
label.text = "[\(x), \(y)]"
newView.addSubview(label)
if y % 2 == 0 {
if x % 2 == 0 {
newView.backgroundColor = UIColor.lightGrayColor()
} else {
newView.backgroundColor = UIColor.blueColor()
}
} else {
if x % 2 == 0 {
newView.backgroundColor = UIColor.blueColor()
} else {
newView.backgroundColor = UIColor.lightGrayColor()
}
}
addSubview(newView)
}
}
}
func alter(type: TypeToAdd, action: AlterAction){
switch type {
case .column:
switch action {
case .add:
columns = columns + 1
case .delete:
columns = columns - 1
}
case .row:
switch action {
case .add:
rows = rows + 1
case .delete:
rows = rows - 1
}
}
setNeedsDisplay()
}
}
You should download and examine the Xcode project. The general idea to dynamically update the UIView is to remove all subviews and then re-add the subviews in drawRect and then call setNeedDisplay whenever you need to update the contents of the GroupView.
Edit:
To access the counts of the subarrays of blockTitles I see two possible solutions.
Use a for loop, define a constant at the beginning of the loop to be equal to the count of the subarray at "i", then put the code inside palletViewConstraint after that and reference the count constant as needed.
Define a variable to hold the count values (an array of Ints) at the start of palletViewConstraint use the high-level function map to append all the blockTitle counts to the previously defined variable, use a for loop and the defined count array to run the rest of your palletViewConstraint.
Examples:
Example 1:
func palletViewConstraint(inputView: UIView, groups: [UIView], blockTitles: [[String]]){
//Do this so the count is not recalculated each for loop
let blockTitlesCount = blockTitles.count
for i in blockTitlesCount {
let subArrCount = blockTitles[i].count
//The rest of your palletViewConstraint Code that references subArrCount
}
}
Example 2:
func palletViewConstraint(inputView: UIView, groups: [UIView], blockTitles: [[String]]){
var countArr = [Int]()
arr.map { (subArr) in
countArr.append(subArr.count)
}
for count in countArr {
//Your code referencing the count of each subArr
}
}
If you need elaboration please ask!

Related

iOS: Collection view custom tag layout dynamic Height Swift

I am trying to create tag list view. Unfortunately I don't know How to work with Custom flow layout. But I found to create pretty tag list. But encountered a problem with multi-line label. If we used the text more than the collection view width, then the attribute.frame.height should be multiplied like 40*numberOfLines. Please help to solve this problem.
Custom Flow Layout:
import UIKit
class CollectionViewRow {
var attributes = [UICollectionViewLayoutAttributes]()
var spacing: CGFloat = 0
init(spacing: CGFloat) {
self.spacing = spacing
}
func add(attribute: UICollectionViewLayoutAttributes) {
attributes.append(attribute)
}
var rowWidth: CGFloat {
return attributes.reduce(0, { result, attribute -> CGFloat in
return result + attribute.frame.width
}) + CGFloat(attributes.count - 1) * spacing
}
func layout(collectionViewWidth: CGFloat) {
var offset = spacing
for attribute in attributes {
attribute.frame.origin.x = offset
offset += attribute.frame.width + spacing
}
}
}
class UICollectionViewCenterLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let attributes = super.layoutAttributesForElements(in: rect) else {
return nil
}
var rows = [CollectionViewRow]()
var currentRowY: CGFloat = -1
for attribute in attributes {
if currentRowY != attribute.frame.midY {
currentRowY = attribute.frame.midY
rows.append(CollectionViewRow(spacing: 10))
}
rows.last?.add(attribute: attribute)
}
rows.forEach { $0.layout(collectionViewWidth: collectionView?.frame.width ?? 0) }
return rows.flatMap { $0.attributes }
}
}
Usage:
let layout = UICollectionViewCenterLayout()
layout.estimatedItemSize = CGSize(width: 140, height: 40)
collectionView.collectionViewLayout = layout
collectionView.reloadData()
I tried to change height but It not working expected:
func layout(collectionViewWidth: CGFloat) {
var offset = spacing
for attribute in attributes {
attribute.frame.origin.x = offset
let attributeTotalWidth = attribute.frame.width + spacing
if attributeTotalWidth > collectionViewWidth{
let multiplier: CGFloat = attributeTotalWidth/collectionViewWidth
let intVal = CGFloat(Int(multiplier))
let fullNumber = multiplier-intVal > 0 ? intVal+1 :
attribute.frame.size.height = fullNumber * 40
}
offset += attributeTotalWidth
}
}
Can you please help me to find out the solution to make variable item height based on the label text content? Thanking you in advance!
#iDeveloper I tried to fix your code.
Here are the things you can do.
Simplify your array.
let titles = ["Apple","Google","Computer", "Terminal", "Gross", "Form lands", "Water lands", "river", "mounts", "trees", "places", "parks", "towns", "cities","Lorem Ipsum is simply dummy text of the printing and typesettingindustry.","Hello","A","B","CAD","John","Nick","Lucas","Amy","Lawrance","Apple","Google","Computer", "Browser","Overflow","Hi","Hello","A","B","CAD","John","Nick","Lucas","Amy","Lawrance", "Lorem Ipsum is simply dummy text of the printing and typesetting industry.",""]
You have to add a width constraint to your title label like this.
Set content hugging priority for you label
End result
One could make the below code prettier by moving more code into Row, but it fixes the issue of clipped labels (as seen in the result image of the other answer). The code is also a lot simpler and still works with labels that contain multiple lines (label.numberOfLines = 0)
class CustomCollectionViewLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let attributes = super.layoutAttributesForElements(in: rect) else {
return nil
}
let collectionViewWidth = collectionView?.frame.width ?? 0
var rows = [Row]()
let spacing: CGFloat = 10
var offsetY: CGFloat = 0
var offsetX: CGFloat = spacing
rows.append(Row())
for attribute in attributes {
let neededWidth = CGFloat(attribute.frame.width) + spacing
let neededMaxX = offsetX + neededWidth
if neededMaxX <= collectionViewWidth {
attribute.frame.origin.x = offsetX
attribute.frame.origin.y = offsetY
offsetX = neededMaxX
rows.last?.add(attribute: attribute)
} else {
attribute.frame.origin.x = spacing
offsetX = attribute.frame.origin.x + neededWidth
offsetY += attribute.frame.height + spacing
attribute.frame.origin.y = offsetY
rows.append(Row())
rows.last?.add(attribute: attribute)
}
}
return rows.flatMap { $0.attributes }
}
}
class Row {
var attributes = [UICollectionViewLayoutAttributes]()
init() {}
func add(attribute: UICollectionViewLayoutAttributes) {
attributes.append(attribute)
}
}

Tether uilabel closely to the text being input to UITextField

I want the currency abbreviation uilabel closely follow text being input into UITextField. What's a good way to
calculate where did the text being input ended so
that
func rightViewRect(forBounds bounds: CGRect) -> CGRect
can calculate the label rect properly?
Among other things I've ended up with this helper:
func rightViewRect(bounds: CGRect,
label: UILabel,
field: UITextField
) -> CGRect
{
let measure = UILabel()
measure.font = field.font
if field.text?.isEmpty ?? true {
measure.text = field.placeholder
} else {
measure.text = field.text
}
let cs = measure.intrinsicContentSize
let lcs = label.intrinsicContentSize
guard lcs.width > 0 else {
return .zero
}
let magicSpace = CGFloat(2)
let unclipped = CGRect(x: cs.width + magicSpace, y: 0, width: lcs.width, height: bounds.height)
let clipped = unclipped.intersection(bounds)
return clipped
}

Table view content disappear on horizontal scroll

I have UIScrollView with 3x Screen Size width, and i have 3 table view in it. For some reason, content disappear in some point of time when i begin to scroll horizontally. But tables are on screen (i can figure that out because i set tables background colour and can see it). Also, i did print items in number of rows and it exist. Here is my code:
func createTables(){
for i in 0...2 {
var tv : UITableView
var adapter : CDRTableAdapter
var tvVm : MainTableViewModel
let offset = (CGFloat(i) * CGFloat(Helper.ScreenSize.width))
print("offset \(offset)")
/* View model */
tvVm = MainTableViewModel()
/* Table view */
adapter = CDRTableAdapter(model:tvVm as CDTableAdapterViewModel, managedVC: self)
let dct = ["EmptyHeightItem" : "EmptyHeightCell", "MenuItem" : "MenuCell"]
tv = UITableView(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
tv.separatorStyle = .none
if (i == 1){
tv.backgroundColor = .yellow
} else {
tv.backgroundColor = .red
}
tv.showsVerticalScrollIndicator = false
tv.mapTableViewWithCellsDictionary(dictionary: dct)
tv.dataSource = adapter
tv.delegate = adapter
tv.estimatedRowHeight = 80
tv.rowHeight = UITableViewAutomaticDimension
self.mainContainerView.addSubview(tv)
tv.snp.makeConstraints { (make) in
make.top.equalTo(self.segmentioView.snp.bottom)
make.bottom.equalTo(self.mainContainerView)
make.left.equalTo(self.mainContainerView).offset(offset)
make.width.equalTo(Helper.ScreenSize.width)
}
}
}
extension UITableView {
func mapTableViewWithCellsDictionary(dictionary : [String : String]) -> Void {
for (key, _) in dictionary {
let cellClassStr = "\(Helper.appName).\(dictionary[key]!)"
self.register(NSClassFromString(cellClassStr), forCellReuseIdentifier: key)
}
}
}
So, if someone has faced the problem before please help.
Thanks

How to calculate the optimal label width for multiline text in swift

I'd like to create a method to calculate the optimal width of a multi-line label to attach several labels in a horizontal row of a fixed height.
With one line of text there is no problem:
let textAttributes: [String : Any] = [NSFontAttributeName: UIFont.preferredFont(forTextStyle: UIFontTextStyle.title2)]
let maximalWidth: CGFloat = text!.boundingRect(
with: CGSize(width: CGFloat.greatestFiniteMagnitude, height: height),
options: [NSStringDrawingOptions.usesLineFragmentOrigin],
attributes: textAttributes,
context: nil).size.width
As far as I understood, there is no option to indicate here, that I have several lines. This method works well in other direction when we calculate the height of the text with the fixed width. But I have the opposite goal.
As a variant, I can create a label based on the longest word (to be more precise, based on the widest word, as we can have several words with the same characters count, but different rendered width):
var sizeToReturn = CGSize()
let maxWordsCharacterCount = text?.maxWord.characters.count
let allLongWords: [String] = text!.wordList.filter {$0.characters.count == maxWordsCharacterCount}
var sizes: [CGFloat] = []
allLongWords.forEach {sizes.append($0.size(attributes: attributes).width)}
let minimalWidth = (sizes.max()! + constantElementsWidth)
I used here two String extensions to create words list and find all longest:
extension String {
var wordList: [String] {
return Array(Set(components(separatedBy: .punctuationCharacters).joined(separator: "").components(separatedBy: " "))).filter {$0.characters.count > 0}
}
}
extension String {
var maxWord: String {
if let max = self.wordList.max(by: {$1.characters.count > $0.characters.count}) {
return max
} else {return ""}
}
}
Not a bad option, but it looks ugly if we have the text that can't be fitted in three lines and that has several short words and one long word at the end. This long word, determined the width, will be just truncated. And more of that it looks not too good with 3 short words like:
Sell
the
car
Well, I have the minimum width, I have the maximum width. Perhaps, I can
go from maximum to minimum and catch when the label starts being truncated.
So I feel that there can be an elegant solution, but I'm stuck.
Hooray, I've found one of the possible solutions. You can use the code below in the playground:
import UIKit
import PlaygroundSupport
//: Just a view to launch playground timeline preview
let hostView = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 480))
hostView.backgroundColor = .lightGray
PlaygroundPage.current.liveView = hostView
// MARK: - Extensions
extension String {
var wordList: [String] {
return Array(Set(components(separatedBy: .punctuationCharacters).joined(separator: "").components(separatedBy: " "))).filter {$0.characters.count > 0}
}
}
extension String {
var longestWord: String {
if let max = self.wordList.max(by: {$1.characters.count > $0.characters.count}) {
return max
} else {return ""}
}
}
// MARK: - Mathod
func createLabelWithOptimalLabelWidth (
requestedHeight: CGFloat,
constantElementsWidth: CGFloat,
acceptableWidthForTextOfOneLine: CGFloat, //When we don't want the text to be shrinked
text: String,
attributes: [String:Any]
) -> UILabel {
let label = UILabel(frame: .zero)
label.attributedText = NSAttributedString(string: text, attributes: attributes)
let maximalLabelWidth = label.intrinsicContentSize.width
if maximalLabelWidth < acceptableWidthForTextOfOneLine {
label.frame = CGRect(origin: CGPoint.zero, size: CGSize(width: maximalLabelWidth, height: requestedHeight))
return label // We can go with this width
}
// Minimal width, calculated based on the longest word
let maxWordsCharacterCount = label.text!.longestWord.characters.count
let allLongWords: [String] = label.text!.wordList.filter {$0.characters.count == maxWordsCharacterCount}
var sizes: [CGFloat] = []
allLongWords.forEach {sizes.append($0.size(attributes: attributes).width)}
let minimalWidth = (sizes.max()! + constantElementsWidth)
// Height calculation
var flexibleWidth = maximalLabelWidth
var flexibleHeight = CGFloat()
var optimalWidth = CGFloat()
var optimalHeight = CGFloat()
while (flexibleHeight <= requestedHeight && flexibleWidth >= minimalWidth) {
optimalWidth = flexibleWidth
optimalHeight = flexibleHeight
flexibleWidth -= 1
flexibleHeight = label.attributedText!.boundingRect(
with: CGSize(width: flexibleWidth, height: CGFloat.greatestFiniteMagnitude),
options: [NSStringDrawingOptions.usesLineFragmentOrigin],
context: nil).size.height
print("Width: \(flexibleWidth)")
print("Height: \(flexibleHeight)")
print("_______________________")
}
print("Final Width: \(optimalWidth)")
print("Final Height: \(optimalHeight)")
label.frame = CGRect(origin: CGPoint.zero, size: CGSize(width: optimalWidth+constantElementsWidth, height: requestedHeight))
return label
}
// MARK: - Inputs
let text: String? = "Determine the fair price"//nil//"Select the appropriate payment method"//"Finalize the order" //"Sell the car"//"Check the payment method"
let font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.callout)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineBreakMode = .byWordWrapping
paragraphStyle.allowsDefaultTighteningForTruncation = true
let attributes: [String:Any] = [
NSFontAttributeName: font,
NSParagraphStyleAttributeName: paragraphStyle,
NSBaselineOffsetAttributeName: 0
]
if text != nil {
let label = createLabelWithOptimalLabelWidth(requestedHeight: 70, constantElementsWidth: 0, acceptableWidthForTextOfOneLine: 120, text: text!, attributes: attributes)
label.frame.width
label.frame.height
label.backgroundColor = .white
label.lineBreakMode = .byWordWrapping
label.numberOfLines = 3
hostView.addSubview(label)
}

Layout subviews not working properly

I have troubles with a custom view that Im designing.
Its essentially a table that display 12 labels, where the upper left label and the lower left label has to be width*5 of the other views. I have already added the views and adjusted the frame in layout subviews, but the labels does not appear in the view (already checked with the new views debugger of Xcode
override func layoutSubviews() {
super.layoutSubviews()
let width = self.frame.size.width
let height = self.frame.size.height
let normalWidth = width/10
let normalHeight = height/2
var currentOrigin = CGPoint(x: 0, y: 0)
let nameSize = CGSize(width: normalWidth * 5 - 3, height: normalHeight)
labels[0][0].frame = CGRect(origin: currentOrigin, size: nameSize)
currentOrigin.x += normalWidth
for j in labels[0]{
j.frame = CGRect(origin: currentOrigin, size: CGSize(width: normalWidth - 3, height: normalHeight))
currentOrigin.x += normalWidth
}
currentOrigin.y = normalHeight
currentOrigin.x = 0
labels[1][0].frame = CGRect(origin: currentOrigin, size: nameSize)
for j in labels[1]{
j.frame = CGRect(origin: currentOrigin, size: CGSize(width: normalWidth - 3, height: normalHeight))
currentOrigin.x += normalWidth
}
}
And this is the constructor that Im using. According to the debugger the views are in the superview but they are not visible
init(frame: CGRect) {
labels = Array(count:2, repeatedValue:Array(count:6, repeatedValue: UILabel() ))
super.init(frame: frame)
for i in 0..labels.count{
for j in 0..labels[i].count{
labels[i][j] = UILabel()
labels[i][j].font = currentFont
labels[i][j].adjustsFontSizeToFitWidth = true
labels[i][j].textAlignment = NSTextAlignment.Center
labels[i][j].text = "HOLA MUNDO"
addSubview(labels[i][j])
}
}
for i in 0..labels.count{
if let k = delegate?{
labels[i][0].text = k.name(i+1)
}
}
for i in 0..labels.count{
for j in 1..labels[i].count{
labels[i][j].text = "0"
}
}
}
In case someone has some similar troubles here is the solution that I finally found
labels = Array(count:2, repeatedValue:Array(count:6, repeatedValue: UILabel() ))
This line generates 2 arrays of UILabels, but all items of the arrays point to the same instance of UILabel. also:
labels[0] === labels[1] //They will point to the same instance
The other mistake was iterating in
for i in 0..labels.count{
if let k = delegate?{
labels[i][0].text = k.name(i+1)
}
}
The correct thing was to iterate from 1 to labels.count as the first label had to have a different size.
The correct form to instanciate the arrays is the following:
for i in 0..2{
labels.append([UILabel]())
for j in 0..6{
labels[i].append(UILabel())
labels[i][j].font = currentFont
labels[i][j].adjustsFontSizeToFitWidth = true
labels[i][j].textAlignment = NSTextAlignment.Center
labels[i][j].text = "HOLA MUNDO"
addSubview(labels[i][j])
}
Hope it help you to avoid this bug. It was really hard to find.

Resources