I am creating an application with a user interface similar to Tinder's. To recreate this user interface, I've had to employ a UIPageViewController embedded in a container view with UIButtons laid over the container view. Recreating this UI is expensive and causes my app to drop in frame rate.
Note the drop in frame rates when swiping to the right page.
Whenever the user scrolls between pages, the overlaid UIButtons adjust in size and color based on how much the user has scrolled between pages. This is done by subclassing the UIPageViewController as a UIScrollViewDelegate and passing the UIPageViewController's UIScrollView's contentOffset.x as a percent of the superview's width to a function that adjusts the overlaid buttons accordingly.
extension PageViewController: UIScrollViewDelegate {
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
let point = scrollView.contentOffset
var percentComplete: CGFloat
percentComplete = abs(point.x - view.frame.size.width)/view.frame.size.width
if !(point.x > view.frame.width && currentPage == 2) && !(point.x < view.frame.width && currentPage == 0) {
if point.x < view.frame.width {
containerVC.adjustButtons(for: percentComplete, direction: 1)
} else {
containerVC.adjustButtons(for: percentComplete, direction: 0)
}
}
}
}
The function that adjusts the size and color of my buttons is called every time scrollViewDidScroll is called. This is done by referencing how much the UIPageViewController has scrolled and adjusting button constraints, image insets, and images accordingly. This function is likely extremely expensive as it is called each time scrollViewDidScroll is called, but I am unsure how else to approach the issue.
The end-result is the behavior that I expected, but there is a considerable drop in frame rate at times when a scroll in the UIPageViewController is begun.
func adjustButtons(for percentage: CGFloat, direction: Int) {
buttonWrapperLeading.constant = (view.frame.width/2 - 38) * percentage
buttonWrapperTrailing.constant = (view.frame.width/2 - 38) * percentage
self.profileButton.imageEdgeInsets = UIEdgeInsets(top: 8 * (1-percentage), left: 8 * (1-percentage), bottom: 8 * (1-percentage), right: 8 * (1-percentage))
self.meetButton.imageEdgeInsets = UIEdgeInsets(top: 8 * percentage, left: 8 * percentage, bottom: 8 * percentage, right: 8 * percentage)
self.chatButton.imageEdgeInsets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
let newProfileButtonImage = UIImage(named: "profileSilhouette")!.withRenderingMode(.alwaysTemplate).image(withTintColor: UIColor.interpolate(from: .darkGray, to: .secondaryColor, with: percentage))
profileButton.setImage(newProfileButtonImage, for: .normal)
let newMeetButtonImage = UIImage(named: "meet")!.withRenderingMode(.alwaysTemplate).image(withTintColor: UIColor.interpolate(from: .secondaryColor, to: .darkGray, with: percentage))
meetButton.setImage(newMeetButtonImage, for: .normal)
}
I am wondering how I could go about this to minimize frame rate dropping. I imagine that the adjustment of constraint constants, as well as setting of button images with a different shade and a different imageInset each time the function is called is likely causing the issue, but I am unsure. I have no clue how else I could recreate this UI without dropping the frame rate.
Here is what Tinder's UI looks like for reference:
I would use profiling (Instruments) to get more details about what is causing the frame drop.
Related
i have view with `UIProgressView` and 3 dot-view. It's like a page control. Each page - the video. `progressView` displays progress of video playback
ok. I do not use constraints for left and right anchors, because my progressView should swap places with dot-view. For example, when current video is ended and next start play, we should swap positions of `progressView` with next dot-view. For swap i just change frames
and the problem is: when i move app to background and returns back, my `progressView` loses his old frame. It attaches to the left side, because `.frame.minX` is 0
and the last one: this problem occurs only after first returns from background
what i tried to do:
save progressView frames before app is going to background and restore it when app comes to foreground: progressView.frame = progressViewOldFrames and call setNeedsDisplay()
add constraint to leftAnchor with constant (frame.minX) before background and remove it after foreground
combine these 2 tries
so now it looks like
func appWillMoveInBackground() {
progressBarXConstraint = progressBar.leftAnchor.constraint(equalTo: self.leftAnchor, constant: progressBar.frame.minX)
progressBarXConstraint?.isActive = true
progressBarFrame = progressBar.frame
}
func updateProgressWidth() {
progressBarXConstraint?.isActive = false
// here i use constraints because my width and height also disables
// and only constraints helps me
progressBar.widthAnchor.constraint(equalToConstant: 32).isActive = true
progressBar.heightAnchor.constraint(equalToConstant: 6).isActive = true
progressBar.frame = progressBarFrame
progressBar.setNeedsDisplay()
}
UPDATE
ok, i should explain more. I guess i cant use constraints because we have some animation while we are scrolling. When we scroll to right - we should move our progressView to some points at right. And in this moment we should move right dot-view to the left. I mean, we do not scroll page by page, we can scroll to a half on the right, then we can return to the initial position.
this code of block did change frames of progressView and dot-view. Formulas are not important. Please, just understand the behavior of this view
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// calc some math variables
// and call method that make changes in frames
pageControl.moveBetweenPages(atPercentValue: distInPercents - 100)
}
// its part of body moveBetweenPages() func
progressBar.frame = CGRect(x: progStartX + progAddDist, y: 0,
width: SizeConstants.progressBarWidth.value,
height: SizeConstants.height.value)
let dotStartX: CGFloat = SizeConstants.progressBarWidth.value + SizeConstants.itemsSpacing.value + (CGFloat(currentPageNum) * dotSectionSize)
dots[currentPageNum].view.frame = CGRect(x: dotStartX - dotAddDist, y: 0,
width: SizeConstants.dotWidth.value,
height: SizeConstants.height.value)
images shows how it looks before background and after
matt from comments suggested me use constraints instead of frames and yes, it helps and it works
only thing i can say is dont forget call setNeedsLayout() after constraints update
I am trying to use MDCChipField, the material design component for Swift.
I am implementing the 'input chip' type and am able to add the entered text as a chip with
let mdcSearchField = MDCSearchField()
mdcSearchField.addChip(chipView)
When the chips overflow in MDCChipField, they get added to next row. How can i set the scrollable direction to horizontal instead of vertical?
In the link,
https://material.io/design/components/chips.html#input-chips,
The placement section explains
Input chips can be integrated with other components. They can appear:
- Inline with the text input cursor in a field
- In a stacked list
- In a horizontally scrollable list
How do i do a a horizontally scrollable list in code?
Thanks.
My approach was this:
Create a scrollable view
create an offset of say 10 for padding between chips
Add chips to the scrollable view
Set the frame origin of the chip with the declared offset
Add the chip's width to the offset plus the initial offset value (in this case 10)
Increase the content width of the scrollable view, set the width to the total
offset
(add more chips/repeat)
On chip removal (if needed)
Set initial offset back to 10
Remove chip from parent scrollable view
Loop through subviews and set the offset again to each of them
Set the scrollable view new width using the total offset
Here is a sample in Swift
Configure initial layout
class ChipSample
{
var tagXOffset = 10
var tagPadding = 10
func configureTagsView()
{
tagView = UIScrollView(frame: CGRect(x: 0, y: 120, width: view.bounds.width, height: 40))
view.addSubview(tagView)
}
Add a chip
func addChip(name:String)
{
let chip = MDCChipView()
chip.titleLabel.text = name
chip.setTitleColor(.gray, for: .normal)
chip.sizeToFit()
chip.addTarget(self, action: #selector(removeChip), for: .touchUpInside)
tagView.addSubview(chip)
chip.frame.origin.x = tagXOffset
chip.frame.origin.y = 0
tagXOffset += tagPadding + chip.frame.width
tagView.contentSize = CGSize(width: tagXOffset, height: tagViewHeight)
}
Remove the chip
#objc func removeChip(sender: MDCChipView!)
{
tagXOffset = tagPadding
sender.removeFromSuperview()
for subview in tagView.subviews {
subview.frame.origin.x = tagXOffset
tagXOffset += tagPadding + subview.frame.width
}
}
I'm trying to implement a Chat screen using UICollectionView in Swift 4.2. I've done it.
I want to improve it by making the cells grow from bottom to top instead of top to bottom (while keeping the order of messages too). Any help? I've tried searching for it, still unsuccessful.
The easiest way would be to flip the collection view and its cells:-
cv.transform = CGAffineTransform(scaleX: 1, y: -1)
cell.contentView.transform = CGAffineTransform(scaleX: 1, y: -1)
Doing this will just flip your CollectionView's content and won't require you to handle anything, I guess
EDIT:-
For your requirement, you shouldn't be appending elements to your array. You should insert new objects as the first element of the array(the data source):-
myArray.insert(element, at: 0)
In order to get the right order, you can just reverse the array
You have to make collection view height constraint outlet for it and calculate your cells height after that you can set height of collectionView. and one case will come of maximum height, your collection height will equal from screen height with calculate space your navigation and input textview.
Have you tried
let diff = collectionView.frame.height - collectionView.contentSize.height
if diff > 0 {
collectionView.contentInset = UIEdgeInsets(top: diff, left: 0, bottom: 0, right: 0)
}
You can modify the top inset of the collection view as the collectionView reloads.
Call this after calling reloadData():
func updateCollectionContentInset() {
let contentSize = yourCollectionView.collectionViewLayout.collectionViewContentSize
var contentInsetTop = yourCollectionView.bounds.size.height
contentInsetTop -= contentSize.height
if contentInsetTop <= 0 {
contentInsetTop = 0
}
yourCollectionView.contentInset = UIEdgeInsets(top: contentInsetTop,left: 0,bottom: 0,right: 0)
}
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)
}
}
}
I trying to create a stretchy table view header and I saw this code on a post online:
override func viewDidLoad() {
tableView.contentInset = UIEdgeInsets(top: kTableHeaderHeight, left: 0, bottom: 0, right: 0)
tableView.contentOffset = CGPoint(x: 0, , y: -kTableHeaderHeight)
updateHeaderView()
}
I am having a little trouble understanding this code.
So essentially what it is doing is (Assuming the screen is 0 to 500 in height and kTableHeaderHeight = 200):
1) It is first adding padding to the top of the tableView by moving it up by kTableHeaderHeight in the contentInset property (this move is with respect to the frame of the tableView). So now does the tableView exists from -200 to 500?
2) Then it moves its bounds up by -kTableHeaderHeight. So does the contentOffset just make it scrollable in the -200 to 500 region? So is that why we are using contentOffset by -kTableHeaderHeight in this case?
1) No, if inset is positive then it makes table view area smaller, like 200,500
2) contentOffset is state of tableview. so when you set offset to -200 it moves content zero point to 200 from table view zero point, regardless content inset. basically it put current table view content to place where it should be regarding inset
So what that code does is reserves 200point place for custom header, that never overlaps with table view cells content (as table view API headers or footers do)