iOS UITextViews data lingering even though I clear out an array - ios

The image below shows Xcode graphical debug hierarchy for a UIViewController. It looks like I need to destroy additional data from UITextViews that are being recreated after editing in an array of UITextViews. Each time I make a change, I've set the array of UITextViews to [], then recreate it with the updated data. Even though I verify that the UITextView array is indeed being reset to zero elements, then recreated with the expected number of elements, these ghost images linger on screen and the debug hierarchy shows something isn't being removed.
I suspect there's some sort of housekeeping I need to do to find & destroy additional data related to the UITextViews, and that setting the array back to zero isn't clearing everything out of my subview, but I'm unsure what this might be. I'm hopeful that my mistake seems obvious to those with more experience & you'll point me in the right direction.
I also share some code below, but I suspect the visual may be the most direct clue for the experienced to set me straight.
var topOfViewFrame: CGFloat = 0
textViewArray = []
for textBlock in textBlocks.textBlocksArray { // a textBlock has formatting data for a UITextView
let textBlockHeight = CGFloat(textBlock.numberOfLines) * textBlock.blockFontSize
let viewFrame = CGRect(x: 0, y: topOfViewFrame, width: textBoxWidth, height: textBlockHeight)
var newTextView = UITextView(frame: viewFrame)
newTextView.center = CGPoint(x: screenView.frame.width/2, y: topOfViewFrame + (textBlockHeight/2)) // screenView is the 320x240 subView holding the [UITextViews]
let viewFont = UIFont(name: "AvenirNextCondensed-Medium", size: textBlock.blockFontSize)
newTextView.font = viewFont
newTextView.text = textBlock.blockText
newTextView = configureTextBlockView(textBoxView: newTextView, textBlock: textBlock)
textViewArray.append(newTextView)
screenView.addSubview(newTextView) // screenView is the subview holding the [UITextViews]
topOfViewFrame += newTextView.frame.height // put the next UITextView directly below the previous one
}
Thanks!

If you want to remove all textViews from 'screenView' its not enough just to clear the array.
You need to remove it from superview:
screenView.subviews.forEach({ $0.removeFromSuperview() })

Related

how to change size of customView passed as UICalanderView Decoration?

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:

Why aren't the backgrounds within these UITextViews clear?

I am attempting to display individual characters in the exact positions that they would appear if displayed as a single string with kerning. The problem is that the characters' bounding boxes seem to be opaque, so that each newly added character covers some of the prior one. Where kerning is greater (e.g., in the combination "ToT"), the problem is obvious:
My setup is something like this: I have an SKView embedded in a container view. In an extension of the SKView's view controller, the following are set within a function in this order:
skView.allowsTransparency = true
scene.backgroundColor = .clear
charAttr – [NSAttributedStringKey.backgroundColor: UIColor.clear]
textView.isOpaque = false
textView.backgroundColor = UIColor.clear
Each UITextView is added successively as a subview to the view (which is an SKView).
I've looked all over my code for some clue as to what could be making the character's bounding boxes seem opaque, but I haven't found anything. The sad thing is that I solved this problem sometime last year, but don't remember what I did and don't have the code anymore.
Any insights or ideas would be appreciated.
After achieving the sought-after effect in a playground, I pasted this simple code into the extension where the original code was. It still worked, so I made it as close to identical to the original as possible, until it also exhibited the problem.
The SKView and extension aspects were irrelevant. The problem lies with how UITextView frames property deals with character widths. Here's the relevant code:
// charInfoArray contains (among other things) an attributed character, its origin,
// its size, and info about whether or not it should be displayed
// For each char, the origin and size info were derived from...
let currentCharRect = layoutManager.boundingRect(forGlyphRange: currentCharRange, in: textContainer)
// To display each (attributed) char of "ToT\n" . . .
for (index, charInfo) in charInfoArray.enumerated() {
guard charInfo.displayed == true else { continue }
let textView = UITextView()
textView.backgroundColor = UIColor.clear
textView.attributedText = charInfo.attrChar
textView.textContainerInset = UIEdgeInsets.zero
// let width = charInfo.size!.width
let width = 30 // arbitrary width
// Using the automatically generated charInfo.size!.width here
// appears to make the text view's background opaque!
textView.frame = CGRect(origin: charInfo.origin!,
size: CGSize(width: width, height: charInfo.size!.height))
textView.frame = textView.frame.offsetBy(dx: 0.0, dy: offsetToCenterY)
textView.textContainer.lineFragmentPadding = CGFloat(0.0)
textView.backgroundColor = UIColor(red: 1, green: 0, blue: 0, alpha: 0.2)
textView.textColor = UIColor.black
view.addSubview(textView)
print(charInfo.size!.width)
}
Setting the width to width = charInfo.size!.width seems to make the text view's background opaque! This may be caused by the unusually large width assigned to newline char at the end of the sequence. To eliminate the problem, I'll have to deal with newline chars in another way. In any case, I have no idea why this causes the text view's background to turn opaque, but it does. Setting the width to an arbitrary value of, say, 30 eliminates problem.
Here are the results of using automatically generated and manually set widths, respectively:
The translucent red areas (on yellow backgrounds) show the bounding rectangles for each of the characters, including the newline character.

iOS 11 breaks row selection

I've recently tested my app in iOS 11 and for some reason I'm not able to select one of the first 12 rows in a dynamically populated table view. The didSelectRow isn't even triggered for these rows. The other rows work fine, but even when scrolling down and back up (the cells should have been re-used again by then) the first 12 rows don't work.
Even on a static table view all cells that appear on screen when switching to that view controller will not respond, neither will controls inside them, even when they are in different sections. Cells that are out of screen initially again work fine.
I'll be trying to test this in an app with boilerplate code, but is this a known bug? I couldn't find anything online about it.
I've tested this after updating the devices to iOS 11, then again from Xcode 9 beta 6 without changes to the code, and again after migrating to Swift 4. Same behaviour inside the simulator. Up to iOS 10 everything is fine, only with iOS 11 the problem occurs.
This will break my app for users in two weeks, I need to fix it, so any help or advice very much appreciated!
UPDATE: As Paulw11 suggested, there is indeed another view blocking the rows. This was notable as row 12 could only be selected in the lower part of the cell, but not in the upper part.
The cause for this issue is the following code:
extension UIViewController {
func setBackgroundImage(forTableView tableView: UITableView) {
let bgImage = UIImage(named: "Background Image.png")
let bgImageView = UIImageView(image: bgImage)
tableView.backgroundView = bgImageView
let rect = bgImageView.bounds
let effect = UIBlurEffect(style: UIBlurEffectStyle.dark)
let blurView = UIVisualEffectView(effect: effect)
let height: CGFloat
switch screenSize.height {
case 480, 568: height = 455
case 736: height = 623
default: height = 554
}
blurView.frame = CGRect(x: 0, y: 0, width: rect.width, height: height)
let container = UIView(frame: rect)
bgImageView.addSubview(blurView)
let bgOverlay = UIImage(named: "Background Overlay.png")
let bgOverlayImageView = UIImageView(image: bgOverlay)
bgOverlayImageView.alpha = 0.15
bgImageView.addSubview(bgOverlayImageView)
self.view.insertSubview(container, at: 1)
}
}
Somehow since iOS 11 this background image seems to be rendered in front of the cells. Not inserting the container view into the table view's view will solve the issue. I've tried setting the zPosition of the container's layer but it does not help. How can I move the background image behind the cells again.
It's weird that this behaviour would change from iOS 10 to 11...
UPDATE 2: Inserting the container at index -1 fixes the issue:
self.view.insertSubview(container, at: -1)
I don't get why this works, though, shouldn't this index be out of range?
UPDATE 3: As Paulw11 pointed out below, the container is completely useless, it was left over from testing and removing it fixes the issue.
The container view seems to be appearing in front of the other views and preventing touches from making it through to the table view.
As an aside, I would see if you can refactor this to use constraints; It always worries me when you see hard-coded screen sizes, as that may break when new devices are released.

How to center the subviews of UIScrollView

I'm a beginner in creating a custom view. I'm trying to create a custom UIView with a scrollview and buttons that will look like this:
I'm adding a view(view with label of page number) inside of scrollView depending on the the number of pages. Is that how it should be?
Currently it looks like this:
My question is how can I center the subviews of scrollview? and next is what's wrong with this code? Why is that I can only see 1 label inside the view? and the other doesn't show up. How can I scroll to the selected page if the page number is not visible already in the scrollview?
Here's my code:
func addPageNumberViewWithCount(count: Int) {
var pageNumberViewX: CGFloat! = 0
let pageNumberViewDistance: CGFloat! = 10
for i in 1...count {
let pageNumberView = UIView(frame: CGRectMake(pageNumberViewX, 0, 30, 30))
pageNumberView.backgroundColor = UIColor.redColor()
pageNumberView.layer.cornerRadius = pageNumberView.frame.height / 2
pageNumberView.layer.masksToBounds = true
pageNumberView.clipsToBounds = true
// add number label
let label = UILabel(frame: CGRectMake(pageNumberViewX, 0, 30, 30))
label.center = pageNumberView.center
label.text = "\(i)"
label.textAlignment = .Center
pageNumberView.addSubview(label)
// update x for next view
pageNumberViewX = pageNumberView.frame.origin.x + pageNumberView.frame.width + pageNumberViewDistance
// add view inside scrollview
scrollView.addSubview(pageNumberView)
if i == count {
scrollView.contentSize = CGSizeMake(pageNumberViewX + pageNumberView.frame.width, 30)
}
}
}
Part of my answer will go to providing a solution to your question,and another part of my answer will go toward strongly suggesting that this not be the method you use to complete your desired tasks.
At this point, AutoLayout and Interface Builder have come a long way. Where they used to be difficult to use because of their inconsistency and unpredictability, they are now highly predictable and consistent as long as you understand the tools and how to use them.
Apple's suggested method for completing this task (which I mostly stand behind) is creating a .xib file (nib) to lay out the base components of the design, and to load the nib into the view or view controller whenever that design should be used. My question for you: have you tried this, or have you determined for some reason that this would be an unsatisfactory solution to your problem? AutoLayout exists to solve these problems not just in allowing you to achieve your desired solution in this one situation but to achieve it in other situations as well, with varying screen sizes and device types.
Now, if you were to simply ignore all of that and continue on your path, there would be a few good ways to handle your problem. One suggested solution I have:
1) Wrap your pageNumberView in another view. Constrain that view to the size of the scrollView. Doing this gives the scrollView content with which to base its scrollable content size, and gives the inner pageNumberView something to compare itself to.
2) Center the pageNumberView horizontally in its container (the new view that we just created).
Doing this, the page numbers should now center themselves in the container until they reach a size where they exceed the width of the scrollView. At that point, they will then continue to expand, making the area horizontally scrollable.
I can provide code examples of how you would do this, but frankly I would much prefer if you scrapped the idea of doing things this way and instead opted for the AutoLayout method at least, and perhaps even the Interface Builder method. I started out with iOS the same way you did, trying to do everything in code. It really isn't the best way to do things, at least with regard to iOS.
Edit: I've provided an example of how this would look in Interface Builder using UINib. I've populated the view with an example of 5 pages to show what it is like. I will see if I can make a GIF or something similar to show what each of the subviews look like.
For the OP, my suggestion would be this: Use this for reference, and go learn the constraints system. It is extremely unlikely that you will find success with iOS if you do not learn and utilize the constraints system. Coding in X values to a UIView's frame is only going to create a product with poor, inconsistent performance across devices, and will take much, much longer than it would to take the time to learn constraints.
Perhaps you should have a UICollectionView with a cell for each of these buttons. That's a better way of doing this, and you can lay it out again when the screen rotates and it changes width.
Those cells will layout offset to the left. You can solve that this way:
let pageNumberViewTotalWidth = 30 * count + (pageNumberViewDistance * count - 1)
self.collectionView.contentInset.left = (self.collectionView.frame.size.width - pageNumberViewTotalWidth) / 2
The labels aren't showing up because you're setting their frame's x to be the same as the page number view's x. It's frame should be relative to it's superview, in this case pageNumberView.
First Question of yours "how can I center the subviews of scrollview?"
Solution: lets suppose you have in total 50 pages and you want to show 5 pages at a time in the scrollview.
Then make 10 subviews of equal widths where each subview width will be equal to visible portion of the collection view that is
self.view.size.width - 2*(width of toggle button)
Then in each container view add 5 of your pageNumberView placed at equal distance
lets pageNumberViewWidth = container.width/5 - 2*margin
now pageNumberView frame will be (margin,0,pageNumberViewWidth,height)
In this way in each container view your pageNumberViews will be placed equally and it will look as if you have centred them.
Second Question "Why is that I can only see 1 label inside the view?"
Answer : Its because you are setting label frame incorrectly
let label = UILabel(frame: CGRectMake(pageNumberViewX, 0, 30, 30))
Here label is the subview of pageNumberView So you have to set its frame according to its parent's view which is pageNumberView, so change it to
let label = UILabel(frame: CGRectMake(0, 0, 30, 30))
First time it was right because pageNumberViewX is 0 for first iteration after that it become some positive value which makes its frame shifted to right but its parent's width is small so its not visible to you.
Third Question : "How can I scroll to the selected page if the page number is not visible already in the scrollview?"
For this you need to find the frame of your selected page:
you can do that by using the offset that you used to create pageNumberView.
(width of each pageNumberView)*pageNumber = starting point of the required pageNumberView.
let frame : CGRect = CGRectMake(calculated offset above, 0,30, 30)
//where you want to scroll
self.scrollView.scrollRectToVisible(frame, animated:true)
I hope this will help you in solving your problem
Edit for first problem
func addPageNumberViewWithCount(count: Int) {
var containerViewX: CGFloat! = 0
let pageNumberViewDistance: CGFloat! = 10
let pageNumberViewPerSubview = 5
var numberOfSubview = count/pageNumberViewPerSubview
if(count % pageNumberViewPerSubview > 0){
numberOfSubview = numberOfSubview + 1
}
var pagesLeft = count
for i in 1...numberOfSubview {
var pageNumberViewX: CGFloat! = 0
let containerView : UIView = UIView(frame:CGRectMake(containerViewX,0,scrollView.frame.size.width,scrollView.frame.size.height))
if(pagesLeft < pageNumberViewPerSubview){
for k in 1...pagesLeft{
}
}
else{
for j in 1...pageNumberViewPerSubview{
let pageNumberView = UIView(frame: CGRectMake(pageNumberViewX, 0, 30, 30))
pageNumberView.backgroundColor = UIColor.redColor()
pageNumberView.layer.cornerRadius = pageNumberView.frame.height / 2
pageNumberView.layer.masksToBounds = true
pageNumberView.clipsToBounds = true
// add number label
let label = UILabel(frame: CGRectMake(0, 0, 30, 30))
label.text = "\(i)"
label.textAlignment = .Center
pageNumberView.addSubview(label)
// update x for next view
pageNumberViewX = pageNumberView.frame.origin.x + pageNumberView.frame.width + pageNumberViewDistance
containerView.addSubview(pageNumberView)
}
containerViewX = containerViewX + scrollView.frame.size.width
// add view inside scrollview
scrollView.addSubview(containerView)
pagesLeft = pagesLeft - pageNumberViewPerSubview
}
if i == count {
scrollView.contentSize = CGSizeMake(numberOfSubview*scrollView.frame.size.width, 30)
}
}
}

Limiting vertical movement of UIAttachmentBehavior inside a UICollectionView

I have a horizontal UICollectionView with a custom UICollectionViewFlowLayout that has a UIAttachmentBehavior set on each cell to give it a bouncy feel when scrolling left and right. The behavior has the following properties:
attachmentBehavior.length = 1.0f;
attachmentBehavior.damping = 0.5f;
attachmentBehavior.frequency = 1.9f;
When a new cell is added to the collection view it's added at the bottom and then animated to its position also using a UIAttachmentBehavior. Naturally it bounces up and down a bit till it rests in its position. Everything is working as expected till now.
The problem I have starts appearing when the collection view is scrolled left or right before the newly added cell has come to rest. The adds left and right bounciness to the up and down one the cell already has from being added. This results in a very weird circular motion in the cell.
My question is, is it possible to stop the vertical motion of a UIAttachmentBehavior while the collection view is being scrolled? I've tried different approaches like using multiple attachment behaviors and disabling scrolling in the collection view till the newly added cell has come to rest, but non of them seem to stop this.
One way to solve this is to use the inherited .action property of the attachment behavior.
You will need to set up a couple of variables first, something like (going from memory, untested code):
BOOL limitVerticalMovement = TRUE;
CGFloat staticCenterY = CGRectGetHeight(self.collectionView.frame) / 2;
Set these as properties of your custom UICollectionViewFlowLayout
When you create your attachment behavior:
UIAttachmentBehavior *attachment = [[UIAttachmentBehavior alloc] initWithItem:item attachedToAnchor:center];
attachment.damping = 1.0f;
attachment.frequency = 1.5f;
attachment.action = ^{
if (!limitVerticalMovement) return;
CGPoint center = item.center;
center.y = staticCenterY;
item.center = center;
};
Then you can turn the limiting function on and off by setting limitVerticalMovement as appropriate.
Have you tried manually removing animations from cells with CALayer's removeAllAnimations?
You'll want to remove the behaviour when the collection view starts scrolling, or perhaps greatly reduce the springiness so that it comes to rest smoothly, but quickly. If you think about it, what you're seeing is a realistic movement for the attachment behaviour you've described.
To keep the vertical bouncing at the same rate but prevent horizontal bouncing, you'd need to add other behaviours - like a collision behaviour with boundaries to the left and right of each added cell. This is going to increase the complexity of the physics a little, and may affect scrolling performance, but it would be worth a try.
Here's how I managed to do it.
The FloatRange limits the range of the attachment, so if you want it to go all the way up and down the screen you just set really large numbers.
This goes inside func recognizePanGesture(sender: UIPanGestureRecognizer) {}
let location = sender.location(in: yourView.superview)
var direction = "Y"
var center = CGPoint(x: 0, y: 0)
if self.direction == "Y" {center.y = 1}
if self.direction == "X" {center.x = 1}
let sliding = UIAttachmentBehavior.slidingAttachment(with: youView, attachmentAnchor: location, axisOfTranslation: CGVector(dx: center.x, dy: center.y))
sliding.attachmentRange = UIFloatRange(minimum: -2000, maximum: 2000)
animator = UIDynamicAnimator(referenceView: self.superview!)
animator.addBehavior(sliding)
If you're using iOS 9 and above then sliding function within attachment class will work perfectly for that job:
class func slidingAttachmentWithItem(_ item: UIDynamicItem,
attachmentAnchor point: CGPoint,
axisOfTranslation axis: CGVector) -> Self
it can be used easily, and it's very effective for sliding Apple documentation
I've resorted to disabling scrolling in the collection view for a specific amount of time after a new cell is added, then removing the attachment behavior after that time has passed using its action property, then adding a new attachment behavior again immediately.
That way I make sure the upwards animation stops before the collection view is scrolled left or right, but also the left/right bounciness is still there when scrolling.
Certainly not the most elegant solution but it works.

Resources