TableView Cell with Minor Lag (Async Proper Use?) Swift - ios

I have an array full of Data loading images in a tableView Cell. However I am getting minor lag(more of a glitch) when the table view scrolls on image index.
Array Contains Data(Seems to lag more bigger the bytes)
Data in Bytes(624230 bytes)
Data in Bytes(1619677 bytes)
Data in Bytes(2257181 bytes)
Data in Bytes(1120275bytes)
Not Sure How to properly use Async When the data is loaded.
struct messageCellStruct {
let message: String!
let photoData: Data!
}
var messageArray = [messageCellStruct]()
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell")
let message2 = cell?.viewWithTag(2) as! UITextView
var photoUploadData = messageArray[indexPath.row].photoData
let main = DispatchQueue.main
let background = DispatchQueue.global()
let helper = DispatchQueue(label: "another_thread")
if(photoUploadData != nil){
print("Data in Bytes\(String(describing: photoUploadData))")
let fullString = NSMutableAttributedString(string: "")
let image1Attachment = NSTextAttachment()
let newImageWidth = (self.message.bounds.size.width - 20 )
let messageDisplayString = self.messageArray[indexPath.row].message
background.async {
image1Attachment.image = UIImage(data: photoUploadData!)
}
image1Attachment.bounds = CGRect.init(x: 0, y: 0, width: newImageWidth, height: 200)
let image1String = NSAttributedString(attachment: image1Attachment)
fullString.append(image1String)
fullString.append(NSAttributedString(string: messageDisplayString!))
message2.attributedText = fullString
message2.textColor = .white
message2.font = UIFont.systemFont(ofSize: 17.0)
}else {
message2.text = self.messageArray[indexPath.row].message
}
return cell!
}
The reason I introduced the Async in the first place was because of the lag. It lags with and without the Async.

You should never perform any UI operations on background thread.
remove background.async from
background.async {
image1Attachment.image = UIImage(data: photoUploadData!)
}
simply use
image1Attachment.image = UIImage(data: photoUploadData!)
EDIT:
The lag is not because of UIImage(data: photoUploadData!) I agree that it is a synchronous call but that wont create a lag the real culprit is let image1String = NSAttributedString(attachment: image1Attachment) NSAttributedString is known to be notorious.
To test the hypothesis you can comment out
let image1String = NSAttributedString(attachment: image1Attachment)
fullString.append(image1String)
fullString.append(NSAttributedString(string: messageDisplayString!))
message2.attributedText = fullString
message2.textColor = .white
message2.font = UIFont.systemFont(ofSize: 17.0)
and you should not see a lag.
Unfortunately I don't know the way to fix the issue with NSAttributedString we had same issue and it would often add up lag on scrolling huge pile of rows. Hence we decided to opt for DTCoreText
Somehow this performs better than NSAttributedString
EDIT:
We concluded that the delay/lag is probably because of conversion of huge data to NSAttributedString. As all that OP wanna do is show image and text below it n he did not know how to handle multiple components in cell I am updating the answer to same.
I am not claiming that this is the only way to do it this way might help the OP is assumption here
Step1:
Create UITableViewCell xib and drag UIImageView to it.
Now imageViews can take implicit size. What does that mean is UIImageView's can grow based on the image shows. If the images you are loading happen to be in your control (Server sending images is yours) and if your backend team can assure that they wont send crazy big images you dont need height constraint to ImageView.
But more often than not, server team claims that its a client team job. Because you would like to show the image best possible way and showing image with aspect fit and let the bigger part of image being chopped off or if image happens to be small leaving the huge space around image ask the backend team to send aspect ratio as a part of the response.
So in that case create a height constraint to imageView
Create an IBOutlet to the height constraint lets assume you call it as imageHeightConstraint
Now when you load the image in cell, you know that imageView's width will be equal to the width of the cell and you know the aspect ratio of the image to be shown so you can calculate the height of supposed image as
either in cellFroRowAtIndexPath
cell.imageHeightConstraint.constant = cell.bounds.size.width * aspectRatioOfImage
or better if you have configure method in a cell where you would expect cell to configure its subviews then
self.imageHeightConstraint.constant = self.bounds.size.width * aspectRatioOfImage
Obviously you might need to convert it to Float from CGFloat am sure u can do it :)
Now if you dont have any control over image and you are downloading it from some random website and hence dont have aspect ratio info then rather than adding height constraint add aspect ratio constraint to imageView and change imageView content mode to .aspectFit this might have side effects I mentioned above but thats the best you get without any support :)
Now add TextView below imageView
add vertical height constraint between imageView and TextView
Select textView and set scroll enabled as false
Select TextView and change vertical content hugging priority and compression resistance to 999
Thats it :)
Now what have you achieved with all these circus is that now you have a TableViewCell which can take implicit size based on content it has without any ambiguity :)
Now that makes the life easy. Implement tableView delegates and dont forget to use
self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.estimatedRowHeight = 44.0;
Thats it :) Now run your code and enjoy self expanding tableView cells based on the content they show :) And you have the layout u wanted
Hope it helps

Related

Append item horizontally into a tableView cell but make them not fill all the space

I have a specific design that I want to achieve, it doesn't seems to complex to me but I'm struggling a lot to make it happen.
First the design looks like this:
So, I have a table view of items which have a picture, then a title and then a collection of three items which will be ImageViews.
All of this inside a cell and inside a stackView.
I tried to make an horizontal stack view and did manage to append my items correctly but it went horribly wrong in terms of design as my imageviews were stretching all the way horizontally. I guess this is not possible to NOT stretch this items.
I also tried to add a collection view into the table view but figured out that was very complex for a thing this basic (as I guess it should be). And then, I'm here.
Here is my code and where I'm stuck at:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = debateList.dequeueReusableCell(withIdentifier: "itemsBox", for: indexPath) as! ItemBox
// Make a variable with 3 participants
let participants = tableView[indexPath.row].participants?.prefix(3)
// iterate over them and adding them to anything that can be doable
participants?.forEach { participant in
let imageView = UIImageView()
let imageUrl = URL(string: participant.imageURL)
imageView.kf.setImage(with: imageUrl)
// Here add item to something
}
return cell
}
I'm stuck and it's getting too long for something this little. Guess I'm a little bit upset with myself for not figuring out how to do it.
If you are having up to 5 participants, then I would suggest you to add by default 2 subviews in horizontal stack view:
empty view with width constraint >= 5 (call it spaceview)
fixed width for "60 % no" UILabel.
then programmatically insert image views with fixed width and aspect ratio 1:1 in horizontal stack view at index 0.
This way, if there will be only 1-2 participants, then the remaining space will be occupied by spaceview as its width will be greater than 5.
If the participants will be more than 5, then better option is to use collectionview with uilabel in horizontal stack view.
First if they are 3 static number of images then it's better to make them inside design and assign the image urls to their outlets respectively
Second for your current work you need to add a width constraint (regarding height they will fit all the stack height when it's a horizontal stack ) like
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.widthAnchor.constraint(equalToConstant: 50).isActive = true

Xcode iOS label resizes UIImage

I am developing an app where I have this problem, regarding showing some products.
The product cell consists of an image and a label under it.
The image and label is inside a UIView because I need corner radius and shadow around the image and label.
But if the label text increases it will resize my image and make it smaller instead of making the parent UIView height larger.
Does anyone know or have an example for that? I want to tell it that it should make the parent UIView bigger instead of making the image inside the UIView smaller.
Image of my problem:
let ratioConstId = "ratio_const"
if let ratioConst = ivPetIcon.constraints.first(where: { $0.identifier == ratioConstId }) {
ivPetIcon.removeConstraint(ratioConst)
}
let ratio = ivPetIcon.image!.size.height / ivPetIcon.image!.size.width
let const = ivPetIcon.heightAnchor.constraint(equalTo: widthAnchor, multiplier: ratio)
const.isActive = true
const.identifier = ratioConstId

Is it possible to set the alignment of segmented Control titles to the left?

I have been looking around for a way to set the alignment of the segmented control titles to the left but I don't seem to be able to achieve what I want.
I have created this little function to change the frame of the subviews of the segment control.
It works at first.
func modifyFrameOfSegment() {
for segment in segmentedControl.subviews {
guard segment.subviews.isNotEmpty else { return }
segment.contentMode = .left
for label in segment.subviews where label is UILabel {
label.frame = CGRect(x: 0, y: label.frame.origin.y, width: label.frame.size.width, height: label.frame.size.height)
(label as! UILabel).textAlignment = .left
}
}
}
But everytime I select a new segment it resets the frames of all the subviews and center align all the titles again.
Is there a way to achieve a permanent left alignment for the segment titles in a segmented control?
Any tips or advice would be greatly appreciated.
Thank you for your time.
Let's use this method
self.segmentedControl.setContentPositionAdjustment(UIOffset(horizontal: -20, vertical: 0), forSegmentType: .left, barMetrics: .default)
And you can do what you want (Of course, you can change the horizontal & vertical value by your needs). Here is the result:
Update:
There's apparently no way to set the alignment of the items, but you can fake it by adjusting the position of each individual item using setContentOffset(_ offset: CGSize, forSegmentAt segment: Int). Here's a kludgy example:
class LeftSegmentedControl: UISegmentedControl {
var margin : CGFloat = 10
override func layoutSubviews() {
super.layoutSubviews()
leftJustifyItems()
}
func leftJustifyItems() {
let fontAttributes = titleTextAttributes(for: .normal)
let segments = numberOfSegments - 1
let controlWidth = frame.size.width
let segmentWidth = controlWidth / CGFloat(numberOfSegments)
for segment in 0...segments {
let title = titleForSegment(at: segment)
setWidth(segmentWidth, forSegmentAt: segment)
if let t = title {
let titleSize = t.size(withAttributes: fontAttributes)
let offset = (segmentWidth - titleSize.width) / 2 - margin
self.setContentOffset(CGSize(width: -offset, height: 0), forSegmentAt: segment)
}
}
}
}
Here's what it looks like:
There are a few caveats:
This version sets the segments to all have equal width, which might not be what you want.
I used a fixed left margin of 10px because it seems unlikely that you'd want to vary that, but you can obviously change it or make it a settable property.
Just because you can do this doesn't mean you should. Personally, I don't think it looks great, and it suffers in the usability department too. Users expect segmented control items to be centered, and left-justifying the items will make it harder for them to know where to tap to hit the segment. That seems particularly true for short items like the one labelled "3rd" in the example. It's not terrible, it just seems a little weird.
Original answer:
UIControl (of which UISegmentedControl is a subclass) has a contentHorizontalAlignment property that's supposed to tell the control to align its content a certain way, so the logical thing to do would be to set it like this:
let segmented = UISegmentedControl(items: ["Yes", "No", "Maybe"])
segmented.frame = CGRect(x:75, y:250, width:250, height:35)
segmented.contentHorizontalAlignment = .left
But that doesn't work — you still get the labels centered. If you've got a compelling use case for left-aligned segments, you should send the request to Apple.
One way you could work around this problem is to render your labels into images and then use the images as the segment labels instead of plain strings. Starting from the code in How to convert a UIView to an image, you could easily subclass UISegmentedControl to create images from the item strings.

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)).

UICollectionView stick to a cell even if it's content offset may change

This is a hard one, I hope I can explain it properly.
All right, so the thing is that I have a UICollectionView where the height of the cell is dynamically changing using collectionView.collectionViewLayout.invalidateLayout() which guides into the sizeForItemAtIndexPath() method of UICollectionVieFlowLayout.
This works perfectly. There is new content, it resizes, moves the other cells according to it, etc. Now, my collection view's content size is probably pretty big, that means when I'm stopping at cell number, say, 23, and cell number 15 is not even displayed but is changing it's size, it is going to make my cell number 23 move up or down. Can I kind of throw an "anchor" to where I currently am? Here's the code that's resizing the cell, triggered by Firebase:
cell.resizeToText(text: text!)
let size = CGSize(width: self.view.frame.width, height: cell.mainView.frame.height + (cell.mainView.frame.origin.y*2.5))
let invalidationIndex = indexPath.row + 1
self.invalidationSizeDictionary[invalidationIndex] = size
let context = UICollectionViewLayoutInvalidationContext()
context.invalidateItems(at: [indexPath])
self.feedCollectionView.collectionViewLayout.invalidateLayout()
Question: is there a possibility to stay at my current offset position in the collectionView?

Resources