Swift - saveContext(), tableView, scrollView, and reloadRows issue? - swift

I have tableView that uses a NSFetchedResultsController to populate data. When clicking on a cell, it takes you to a detailViewController of that object. And the following two properties are pushed forward with prepare(for:).
var coreDataStack: CoreDataStack!
var selectedGlaze: Glaze?
Inside the detailView, I have 2 cells. The first is cell that contains a scrollView with an array of images:
import UIKit
protocol SwipedRecipeImageViewDelegate: class {
func recipeImageViewSwiped(_ cell: RecipePhotoTableViewCell, selectInt: Int)
}
class RecipePhotoTableViewCell: UITableViewCell, UIScrollViewDelegate {
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var pageControl: UIPageControl!
// -
var imagesArray: [Data] = []
var selectedImageData: Int = 0
// -
weak var delegate: SwipedRecipeImageViewDelegate?
// -
override func awakeFromNib() {
super.awakeFromNib()
self.backgroundColor = .clear
self.selectionStyle = .none
scrollView.delegate = self
scrollView.isUserInteractionEnabled = false // Allows didSelectAtRow:
contentView.addGestureRecognizer(scrollView.panGestureRecognizer) // Allows Scrolling
}
override func layoutSubviews() {
super.layoutSubviews()
setImages()
setOffsetX(pageNumber: selectedImageData)
}
func configureCell(section: Int, row: Int, images: [RecipeImage], arrayInt: Int, delegate: SwipedRecipeImageViewDelegate) {
self.delegate = delegate
selectedImageData = arrayInt
for image in images {
guard let imageData = image.recipeImageData else { return }
imagesArray.append(imageData)
}
}
#IBAction func pageChanged(_ sender: UIPageControl) {
setOffsetX(pageNumber: sender.currentPage)
}
func setOffsetX(pageNumber: Int) {
pageControl.currentPage = pageNumber
let offsetX = contentView.bounds.width * CGFloat(pageNumber)
DispatchQueue.main.async {
UIView.animate(withDuration: 0.2, delay: 0, options: UIView.AnimationOptions.curveEaseOut, animations: {
self.scrollView.contentOffset.x = offsetX
}, completion: nil)
}
}
func setImages() {
// Set Page Count:
pageControl.numberOfPages = imagesArray.count
// Set Frame For ImageViews + Scroll View:
for index in 0..<imagesArray.count {
let imageView = UIImageView()
imageView.frame.size = contentView.bounds.size
imageView.frame.origin.x = contentView.bounds.width * CGFloat(index)
imageView.image = UIImage(data: imagesArray[index])
imageView.contentMode = .scaleAspectFill
imageView.layer.masksToBounds = true // Limits Frame Size
scrollView.addSubview(imageView)
}
// Set ScrollView Size:
scrollView.contentSize = CGSize(width: (contentView.bounds.width * CGFloat(imagesArray.count)), height: contentView.bounds.height)
scrollView.delegate = self
}
// Set Page Number:
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let pageNumber = scrollView.contentOffset.x / scrollView.frame.size.width
self.pageControl.currentPage = Int(pageNumber)
delegate?.recipeImageViewSwiped(self, selectInt: pageControl.currentPage)
}
The second cell contains a stackView with some labels to display data that the image shows. It accepts a lot of parameters and then sets the textColor and changes some labels. Nothing too exciting so I didn't include the code.
DetailViewController:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
print("cellForRowAt: ", indexPath)
switch indexPath.section {
case sectionImage: // Section 0:
guard
let images = selectedGlaze?.glazeImage,
let glazeImageSelected = selectedGlaze?.glazeImageSelected // This is a Double
else { return returnDefaultCell() }
let imageArray = images.allObjects as! [RecipeImage] // Takes NSSet of relational data and changes it into an Array to be passed into the image cell.
let imageSelected = Int(glazeImageSelected) // Double Converted to Int
switch indexPath.row {
case 0:
let cell = returnRecipeImageCell()
return configureRecipeImageCell(cell: cell, for: indexPath, imagesArray: imageArray, imageSelected: imageSelected)
case 1:
let cell = returnAtmosphereCell()
return configureAtmosphereCell(cell: cell, for: indexPath, imagesArray: imageArray, imageSelected: imageSelected)
default: return returnDefaultCell()
}
}
}
SwipedRecipeImageViewDelegate:
func recipeImageViewSwiped(_ cell: RecipePhotoTableViewCell, selectInt: Int) {
selectedGlaze?.glazeImageSelected = Double(selectInt)
coreDataStack.saveContext()
DispatchQueue.main.async { //
self.tableView.beginUpdates() //
let row1: IndexPath = [0,1] //
self.tableView.reloadRows(at: [row1], with: .automatic) //
self.tableView.endUpdates() //
}
}
The Issue:
The issue I'm having is reloading the second cell to be updated with the correct information after the recipeImageViewSwiped() is called. Seen here: https://imgur.com/a/fIYfehf
This happens when the code inside the DispatchQueue.main.async block is active. When the block is comment out, this happens: https://imgur.com/a/fYUVZKH - Which is what I'd expect. (Other than the cell at [0,1] isn't updated).
Specifically, when the tableView reloads row [0,1], cellForRowAt() only gets called on that row, [0,1]. But I'm not sure why the cell at [0,0], with the image, flicks back to the original image shown in the scrollView.
Goal:
My goal is to have the cell with the scrollView not flicker after being swiped on. But also to save the context, so that the object can save which image in the array is selected. And then to update/reload the second cell with the new information the image that's selected, so it can update it's labels correctly.
EDIT:
Removing the following in layoutSubviews() has this affect: https://imgur.com/a/vwrZfus - Which looks like it's mostly working. But still has a strange animation.
override func layoutSubviews() {
super.layoutSubviews()
setImages()
// setOffsetX(pageNumber: selectedImageData)
}
EDIT 2:
This looks like its entirely an issue with setting up the cell's view. Along with layout Subviews.
EDIT 3:
I added a Bool: hasSetLayout and a switch inside of layoutSubviews() and it appears to be working as I want. - However if any one still has any information to help me understand this issue, I'd appreciate it.
var hasSetLayout = false
override func layoutSubviews() {
super.layoutSubviews()
switch hasSetLayout {
case false: setImages(selectedPhoto: selectedImageData)
default: break
}
}

try to reload row without animation :
UIView.performWithoutAnimation {
tableView.reloadRows(at: [indexPath], with: .none)
}

Related

Duplicating UIViews in StackView when scrolling TableView Swift

Issue: When I scroll the tableView, my configureCell() method appends too many views to the stackView inside the cell.
When the cell is first displayed, and I press the UIButton, the stack is unhidden and first shows the right amount of views, after scrolling, the amount is duplicated.
prepareForReuse() is empty right now. I want to keep the stackView unHidden after scrolling.
I set the heightAnchor for the UIView as it is programmatic layout.
Expected Outcome: User taps on the button, the cell stackView is unhidden and the cell expands to chow the uiviews related to the cell.
When I call it in the cellForRowAt, nothing happens. Because im not sure how to modify the method for IndexPath.row.
protocol DataDelegate: AnyObject {
func displayDataFor(_ cell: TableViewCell)
}
class TableViewCell: UITableViewCell {
#IBOutlet weak var stackView: UIStackView! {
didSet {
stackView.isHidden = true
}
}
#IBOutlet button: UIButton!
var model = Model()
var detailBool: Bool = false
#IBAction func action(_ sender: Any) {
self.claimsDelegate?.displayClaimsFor(self)
detailBool.toggle()
}
func configureCellFrom(model: Model) {
if let addedData = model.addedData {
if addedData.count > 1 {
for data in addedData {
let dataView = DataView()
dataView.heightAnchor.constraint(equalToConstant: 65).isActive = true
dataView.dataNumber.text = data.authDataNumber
self.stackView.addArrangedSubview(dataView)
}
}
}
}
}
How would I call this in cellForRowAt, so its only created the correct amount of uiviews and not constantly adding?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: TableViewCell.identifier, for: indexPath) as? TableViewCell else {
return UITableViewCell()
}
cell.dataDelegate = self
cell.configureCellFrom(model: model[indexPath.row])
//if let addedData = model.addedData {
//if addedData.count > 1 {
//for data in addedData {
//let dataView = DataView()
//dataView.heightAnchor.constraint(equalToConstant: 65).isActive = true
//dataView.dataNumber.text = data.authDataNumber
//self.stackView.addArrangedSubview(dataView)
// }}}
cell.layoutIfNeeded()
cell.selectionStyle = .none
return cell
}
extension ViewController: DataDelegate {
func displayDataFor(_ cell: TableViewCell) {
if tableView.indexPath(for: cell) != nil {
switch cell.detailBool {
case true:
UIView.performWithoutAnimation {
tableView.beginUpdates()
cell.detailArrow.transform = CGAffineTransform(rotationAngle:.pi)
cell.stackView.isHidden = false
tableView.endUpdates()
}
case false:
UIView.performWithoutAnimation {
tableView.beginUpdates()
cell.stackView.isHidden = true
cell.detailArrow.transform = CGAffineTransform.identity
tableView.endUpdates()
}
}
}
}
}
If I understand correctly, you would like to create an expandable TableView? If yes you can do it a lot of different ways, but you have to change your approach totally. Please refer LBTA approach:
LBTA video
My favourite the Struct approach, where you create a struct and you can save the complication with the 2D array:
Struct stackoverflow link

SwiftUI view sizing is broken when added to UITableViewController

I need to display a SwiftUI view in an existing UITableViewController, as the tableHeaderView. However, it seems that the sizing of the SwiftUI view is broken when it is added to a UITableViewController.
If I simply convert my SwiftUI View to a UIView using a UIHostingController and set it to the tableHeaderView, the view is displayed off screen:
func addHeaderView() {
let view = VerticalTextStack()
let hostingController = UIHostingController(rootView: view)
tableView.tableHeaderView = hostingController.view
}
Incorrect layout
To counteract this, I've tried to fix the height of the view several different ways. Adding an NSLayoutConstraint didn't do anything. When setting tableHeaderView.frame.size manually, the results were better, since at least now the view is displayed on-screen, but the multiline Texts become single-line and truncated.
As you can see here, the 2nd Text gets truncated:
Here's a simplified example showcasing the problem:
/// `UITableViewController` displaying a `UIView` as its `tableHeaderView`
class TableViewController: UITableViewController {
let cellReuseIdentifier = "cell"
let data = ["A", "B", "C", "D", "E"]
let themeManager = AppThemeManager()
// MARK: - UIViewController lifecycle
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellReuseIdentifier)
addHeaderView()
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
fixTableHeaderViewSize()
}
// MARK: - UITableViewDelegate
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
data.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier, for: indexPath)
cell.textLabel?.text = data[indexPath.row]
return cell
}
// MARK: - SwiftUI view
func fixTableHeaderViewSize() {
guard let tableHeaderView = tableView?.tableHeaderView else { return }
let expectedHeight = tableHeaderView.systemLayoutSizeFitting(UIView.layoutFittingExpandedSize).height
let expectedSize = CGSize(width: tableHeaderView.frame.width, height: expectedHeight)
tableHeaderView.frame.size = expectedSize
}
func addHeaderView() {
let view = VerticalTextStack()
let hostingController = UIHostingController(rootView: view)
tableView.tableHeaderView = hostingController.view
}
}
private struct VerticalTextStack: View {
let data = ["First", "I am a very long text that only fits in multiple lines. I still continue.", "Third"]
let themeManager = AppThemeManager()
var body: some View {
VStack(spacing: 10) {
ForEach(data, id: \.self) { value in
Text(value)
}
}
}
}
I've also tried moving addHeaderView to other UIViewController functions, such as viewWillLayoutSubviews, but that didn't change anything.
Setting lineLimit to nil or any large number on the Text inside VerticalTextStack and adding .layoutPriority(.greatestFiniteMagnitude) to the Text did not make the Text multiline either.
Here is a possible solution.
Change your add header view funcation with this.
func addHeaderView() {
let view = VerticalTextStack()
let hostingController = UIHostingController(rootView: view)
let headerViewMain = UIView()
headerViewMain.backgroundColor = .red
headerViewMain.addSubview(hostingController.view)
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
let constraints = [
hostingController.view.topAnchor.constraint(equalTo: headerViewMain.topAnchor),
hostingController.view.leftAnchor.constraint(equalTo: headerViewMain.leftAnchor),
headerViewMain.bottomAnchor.constraint(equalTo: hostingController.view.bottomAnchor),
headerViewMain.rightAnchor.constraint(equalTo: hostingController.view.rightAnchor)
]
NSLayoutConstraint.activate(constraints)
headerViewMain.frame.size.height = headerViewMain.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height
headerViewMain.frame.size.width = headerViewMain.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).width
self.tableHeaderView = headerViewMain
self.layoutIfNeeded()
self.setNeedsLayout()
self.reloadData()
}

`UITableViewCell` call function from parent TableView

I have a UI widget as follows
class ProfileBubbleCell: UITableViewCell {
var roundImageView: UIImageView?
override func awakeFromNib() {
super.awakeFromNib()
self.contentView.backgroundColor = Color.red
initImage()
}
private func initImage(){
let imageView = UIImageView()
let width = self.frame.width
let height = self.frame.height
let img_width = height - 4
let img_height = img_width
let y = 2
let x = width/2 - img_width/2
imageView.frame = CGRect(
x: x, y: CGFloat(y), width: img_width, height: img_height
)
let rounded = imageView
.makeRounded()
.border(width:2.0, color:Color.white.cgColor)
// attach and save reference
self.addSubview(rounded)
self.roundImageView = rounded
}
private func loadImage(){
// #TODO: call parent function
}
}
And in loadImage, I would like to call the parent's image loading view, and when the image is loaded, display it in roundImageView. ProfileBubbleCell is really meant to be as generic as possible, its only concern is making the image round and centering it.
This looks like a very common use case and I would like to delegate the loading image task to the parent, but I am not sure how to express it.
In the parent I instantiate the cell as follows:
let cell = tableView.dequeueReusableCell(withIdentifier: "ProfileBubbleCell", for: indexPath) as! ProfileBubbleCell
Here show you some about delegate use.
// 1) define delegate.
protocol ProfileBubbleCellDelegate {
func getImage() -> UIImage?
}
class ProfileBubbleCell: UITableViewCell {
// 2) declare a variable of ProfileBubbleCellDelegate
weak var delegate: ProfileBubbleCellDelegate?
//
func configure() {
self.roundImageView.image = delegate.getImage()
}
}
// when dequeueReuseCell on cellForRow(at:)
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "ProfileBubbleCell", for: indexPath) as ProfileBubbleCell else { return UITableView() }
// 3) assign delegate to tableView's superView, maybe it's a UIViewController or UIView on your class.
cell.delegate = self
cell.configure()
return cell
}
// 4) extension Your class who be assigned delegate of ProfileBubbleCellDelegate
// and implement delegate's method.
extension YourClass: ProfileBubbleCellDelegate {
func getImage() -> UIImage? {
// 5) provide image.
return hereProvideYourImage
}
}
// or if you want immediately download image when cell.roundedImageView need it.
// modify the getImage() of Delegate like this.
protocol ProfileBubbleCellDelegate {
func getImage(completion: #escaping ((UIImage?) -> Void))
}
// then the extension implement will be
extension YourClass: ProfileBubbleCellDelegate {
func getImage(completion: #escaping ((UIImage?) -> Void)) {
downloadImageTask.downloadImage(url: imageUrl, completion: { downloadedImage in
// because completion will be passed on other closure of downloadImage(completion:),
// so delegate method need add `#escaping` that means the completion can escape from itself closure.
completion?(downloadedImage)
})
}
}
// don't forget replace called method on cell.
class ProfileBubbleCell: UITableViewCell {
// ...
func configure() {
delegate.getImage(completion: { downloadedImage in
DispatchQueue.main.async {
self.roundImageView.image = downloadedImage
}
})
}
}

Implementing UIStackView inside UICollectionViewCell

I am trying to display user ratings on the collectionview as follows with using two images, half.png and full.png.
But even though I hard coded with 1 star to test, but sometimes it shows more than 1 star during the scroll. It keeps increasing.
I wonder how to handle this issue?
CollectionViewCell
import UIKit
class CollectionCell: UICollectionViewCell {
#IBOutlet var ratingView: UIStackView!
var rating : Float!
{
didSet {
self.ratingView.addArrangedSubview(addRating())
}
}
func addRating() -> UIStackView
{
let stackView = UIStackView()
stackView.axis = .horizontal
let oneStarImage = UIImage(named: "full_star")
let halfStarImage = UIImage(named: "half_star")
//hard coded
var value = 1.0
while true {
value -= 1
if value >= 0 {
print("Add 1 star")
let imageView = UIImageView()
imageView.image = oneStarImage
stackView.addArrangedSubview(imageView)
} else if value == -0.5 {
print("Add half a star")
let imageView = UIImageView()
imageView.image = halfStarImage
stackView.addArrangedSubview(imageView)
break
}
else {
break
}
}
return stackView
}
}
CollectionViewController
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellId", for: indexPath) as! CollectionViewCell
cell.rating = 1.0
return cell
}
As UICollectionViewCell is reused, every time you set the rating, a new view is added to the UIStackView. Therefore, you should reset the UIStackView before adding the images. Also, it maybe unnecessary to create a UIStackView every time you add a rating. You can reuse the ratingView instead. So, the complete code should be:
var rating : Float!
{
didSet {
self.addRating()
}
}
func addRating()
{
// Empty the ratingView first
ratingView.arrangedSubviews.forEach { $0.removeFromSuperview() }
let oneStarImage = UIImage(named: "full_star")
let halfStarImage = UIImage(named: "half_star")
//hard coded
var value = 1.0
while true {
value -= 1
if value >= 0 {
print("Add 1 star")
let imageView = UIImageView()
imageView.image = oneStarImage
ratingView.addArrangedSubview(imageView)
} else if value == -0.5 {
print("Add half a star")
let imageView = UIImageView()
imageView.image = halfStarImage
ratingView.addArrangedSubview(imageView)
break
}
else {
break
}
}
}
And also you can use for reused UICollectionViewCell or UITableViewCell,
override func prepareForReuse() {
super.prepareForReuse()
self.ratingView = UIStackView()
}
override func to prepare cell to reuse. However you should re init your components that you assign values in cell.

Flexible editable UITextView in UITableView: won’t show the last line under the keyboard

I have a flexible editable UITextView in a UITableViewCell. The complete source code of a simple project can be found at https://github.com/AlexChekanov/TextViewInTableView
Everything works fine. I recalculate the cell height each time the TextView height changes and I scroll the table to the cursor.
class TextViewTableViewCell: UITableViewCell, UITextViewDelegate {
weak var tableView: UITableView?
var textViewHeight: CGFloat?
#IBOutlet weak var textView: UITextView!
override func awakeFromNib() {
super.awakeFromNib()
textViewHeight = textView.intrinsicContentSize.height
textView.becomeFirstResponder()
}
func textViewDidChangeSelection(_ textView: UITextView) {
guard let tableView = tableView else { return }
selfUpdate(in: tableView)
scrollToCursor()
}
func selfUpdate(in tableView: UITableView) {
// Do nothing if the height wasn't change
guard textViewHeight != textView.intrinsicContentSize.height else { return }
textViewHeight = textView.intrinsicContentSize.height
// Disabling animations gives us our desired behaviour
UIView.setAnimationsEnabled(false)
/* These will causes table cell heights to be recaluclated,
without reloading the entire cell */
tableView.beginUpdates()
tableView.endUpdates()
UIView.setAnimationsEnabled(true)
}
func scrollToCursor() {
guard let tableView = tableView else { return }
if let currentCursorPosition = textView.selectedTextRange?.end {
print(currentCursorPosition)
let caret = textView.caretRect(for: currentCursorPosition)
print(caret)
tableView.scrollRectToVisible(caret, animated: false)
}
}
}
The only problem is that when I add the last line or several empty lines at the bottom, table view doesn't scroll. But if I add any symbol to this empty line, it scrolls.
Thank you for help.
UITextView's allow scrolling by default which could possibly be the issue. When the size of the text within the UITextView is larger than the UITextView itself, it allows you to scroll. This may be conflicting with the UITableView's scrolling. Simply use this to be safe:
self.textView.isScrollEnabled = false
self.textView.isEditable = false
I found a solution:
class TextViewTableViewCell: UITableViewCell, UITextViewDelegate {
weak var tableView: UITableView?
var textViewHeight: CGFloat?
#IBOutlet weak var textView: UITextView!
override func awakeFromNib() {
super.awakeFromNib()
textViewHeight = textView.intrinsicContentSize.height
textView.becomeFirstResponder()
}
func textViewDidChangeSelection(_ textView: UITextView) {
guard let tableView = tableView else { return }
selfUpdate(in: tableView)
// Here is the trick!
if textView.textStorage.string.hasSuffix("\n") {
CATransaction.setCompletionBlock({ () -> Void in
self.scrollToCaret(textView, animated: false)
})
} else {
self.scrollToCaret(textView, animated: true)
}
}
func scrollToCaret(_ textView: UITextView, animated: Bool) {
guard let tableView = tableView else { return }
guard let currentCursorPosition = textView.selectedTextRange?.end else { return }
var caret = textView.caretRect(for: currentCursorPosition)
caret.size.height += textView.textContainerInset.bottom * 2
tableView.scrollRectToVisible(caret, animated: animated)
}
func selfUpdate(in tableView: UITableView) {
// Do nothing if the height wasn't change
guard textViewHeight != textView.intrinsicContentSize.height else { return }
textViewHeight = textView.intrinsicContentSize.height
// Disabling ansimations gives us our desired behaviour
UIView.setAnimationsEnabled(false)
/* These will causes table cell heights to be recaluclated,
without reloading the entire cell */
tableView.beginUpdates()
tableView.endUpdates()
UIView.setAnimationsEnabled(true)
}
}