I'm trying to create a page browser by using a UIPageViewController in Interface Builder that allows displaying part of the adjacent pages (aka peeking). I've been following a tutorial at http://www.appcoda.com/uipageviewcontroller-storyboard-tutorial/ (and ported it into Swift) which is rather straightforward but I can't quite figure out what changes to make to have a page displayed in the UIPageViewController which is smaller than the screen (and centered) and having the adjacent pages appear partly on the screen left and right.
I've tried to resize the page content view controller in IB and with code but the page view controller will still fill the whole screen.
Does anyone know of a tutorial that covers this functionality or what is a good approach to get the desired effect?
This screenshot below from Bamboo Paper shows what I'm trying to achieve...
Like #davew said, peeking views will need to use UIScrollView. I too searched for a way to use UIPageViewController but couldn't find any resource.
Using UIScrollView to make this feature was less painful that I had imagined.
Here is a simple example to see the basic controls in action.
First: make a UIViewController, then in the viewDidLoad method, add the following code:
float pad = 20;
NSArray* items = #[#"One", #"Two", #"Three", #"Four"];
self.view.backgroundColor = [UIColor greenColor];
UIScrollView* pageScrollView = [[UIScrollView alloc] initWithFrame:self.view.frame];
pageScrollView.opaque = NO;
pageScrollView.showsHorizontalScrollIndicator = NO;
pageScrollView.clipsToBounds = NO;
pageScrollView.pagingEnabled = YES;
adjustFrame(pageScrollView, pad, deviceH()/4, -pad*3, -deviceH()/2);
[self.view addSubview: pageScrollView];
float w = pageScrollView.frame.size.width;
for(int i = 0; i < [items count]; i++){
UIView* view = [[UIView alloc] initWithFrame:pageScrollView.bounds];
view.backgroundColor = [UIColor blueColor];
setFrameX(view, (i*w)+pad);
setFrameW(view, w-(pad*1));
[pageScrollView addSubview:view];
}
pageScrollView.contentSize = CGSizeMake(w*[items count], pageScrollView.frame.size.height);
FYI, I used these util functions to adjust the size of the view frames; I get sick of manually changing them with 3+ lines of code.
Update
I have wrapped up this code in a simple ViewController and put it on GitHub
https://github.com/kjantzer/peek-page-view-controller
It is in no way complete, but it's a working start.
I was just searching for a good solution to the same feature. I found a nice tutorial on Ray Wenderlich's site titled "How To Use UIScrollView to Scroll and Zoom Content". It illustrates multiple things you can do with UIScrollView. The fourth and final is "Viewing Previous/Next Pages" which is your "peek" feature.
I haven't implemented this yet, but the key steps seem to be:
Create a UIScrollView narrower than your screen
Turn on Paging Enabled
Turn off Clip Subviews
Fill the UIScrollView with your pages side by side
Embed the UIScrollView inside a UIView that fills the width of the screen so that you can capture and pass touches outside the Scroll View
I rewrite Kevin Jantzer answer in swift 4 and it's works!
override func viewDidLoad() {
super.viewDidLoad()
let pad: CGFloat = 20
let items: [UIColor] = [.blue, .yellow, .red, .green]
self.view.backgroundColor = .white
var pageScrollView = UIScrollView(frame: self.view.frame)
pageScrollView.isOpaque = false
pageScrollView.showsHorizontalScrollIndicator = false
pageScrollView.clipsToBounds = false
pageScrollView.isPagingEnabled = true
adjustFrame(myView: pageScrollView, x: pad, y: UIScreen.main.bounds.height / 4, w: -pad * 3, h: -UIScreen.main.bounds.height/2)
self.view.addSubview(pageScrollView)
let w = pageScrollView.frame.size.width
for (i, item) in items.enumerated() {
let myView = UIView(frame: pageScrollView.bounds)
myView.backgroundColor = item
setFrameX(myView: myView, x: (CGFloat(i) * w) + pad);
setFrameW(myView: myView, w: w-(pad*1));
pageScrollView.addSubview(myView)
}
pageScrollView.contentSize = CGSize(width: w * CGFloat(items.count), height: pageScrollView.frame.size.height);
}
func setFrame(myView: UIView, x: CGFloat?, y: CGFloat?, w: CGFloat?, h: CGFloat?){
var f = myView.frame
if let safeX = x {
f.origin = CGPoint(x: safeX, y: f.origin.y)
}
if let safeY = y {
f.origin = CGPoint(x: f.origin.x, y: safeY)
}
if let safeW = w {
f.size.width = safeW
}
if let safeH = h {
f.size.height = safeH
}
myView.frame = f
}
func setFrameX(myView: UIView, x: CGFloat) {
setFrame(myView: myView, x: x, y: nil, w: nil, h: nil)
}
func setFrameY(myView: UIView, y: CGFloat) {
setFrame(myView: myView, x: nil, y: y, w: nil, h: nil)
}
func setFrameW(myView: UIView, w: CGFloat) {
setFrame(myView: myView, x: nil, y: nil, w: w, h: nil)
}
func setFrameH(myView: UIView, h: CGFloat) {
setFrame(myView: myView, x: nil, y: nil, w: nil, h: h)
}
func adjustFrame(f: CGRect, x: CGFloat?, y: CGFloat?, w: CGFloat?, h: CGFloat?) -> CGRect {
var rect = f
if let safeX = x {
rect.origin = CGPoint(x: rect.origin.x + safeX, y: f.origin.y)
}
if let safeY = y {
rect.origin = CGPoint(x: f.origin.x, y: rect.origin.y + safeY)
}
if let safeW = w {
rect.size.width = safeW + rect.size.width
}
if let safeH = h {
rect.size.height = safeH + rect.size.height
}
return rect
}
func adjustFrame(myView: UIView, x: CGFloat, y: CGFloat, w: CGFloat, h: CGFloat) {
myView.frame = adjustFrame(f: myView.frame, x: x, y: y, w: w, h: h);
}
}
Related
I am trying to allow the user to rotate text they place on a view inside UITextViews. The problem is when I rotate them using the transform property, the text gets cutoff. However, when I do this with UILabels, it works fine, so why not UITextViews? The bounds of UITextView (shown in green) never changes, so I am not sure why this is happening. The amount of cutoff seems related to the height of the text view.
The only work around I've found is to do the transform asynchronously. I don't know why it works, but I don't think it's a good solution. You can see this in action with the following code.
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
for xPos in stride(from: 80, to: view.bounds.width, by: 80) {
let point = CGPoint(x: xPos, y: 100)
let textView = makeTextView(at: point)
//DispatchQueue.main.async {
let angle = xPos / 400.0
textView.transform = CGAffineTransform(rotationAngle: angle)
//}
view.addSubview(textView)
}
}
private func makeTextView(at point: CGPoint) -> UITextView {
let textView = UITextView()
textView.text = "1234567890"
textView.isScrollEnabled = false
textView.backgroundColor = .green
textView.center = CGPoint(x: point.x, y: point.y)
textView.bounds.size = CGSize(width: 80, height: 24)
return textView
}
}
What is the correct way to rotate them?
How can I achieve the dynamic no of items with dynamic content size one after other and in next line if the content gets more than superview in a custom table view cell?
Please refer image for question clarification.
The question you posted is a bit long and actually requests for multiple things since you didn't post what you have tried and what part you are having trouble with. There are many ways of doing this so let's start with normal label:
An UILabel can have attributed string under attributedText which can be used to achieve the visual design you are showing in your image. To lay them out horizontally we could use sizeToFit to get their width. Assume something like:
func layoutLabels(_ labels: [UILabel], inView view: UIView, horizontalSeparator: CGFloat = 4.0) {
var x: CGFloat = 0.0
labels.forEach { label in
label.sizeToFit() // Will make it's size just right
label.frame = CGRect(x: x, y: 0.0, width: label.frame.width, height: label.frame.height)
x = label.frame.maxX + horizontalSeparator
view.addSubview(label)
}
}
Now to add vertical part we simply need to add maximum width and also add y component:
func layoutLabels(_ labels: [UILabel], inView view: UIView, maximumWidth: CGFloat, lineHeight: CGFloat, horizontalSeparator: CGFloat = 4.0, verticalSeparator: CGFloat = 2.0) {
var x: CGFloat = 0.0
var y: CGFloat = 0.0
labels.forEach { label in
label.sizeToFit() // Will make it's size just right
var newX = x + label.frame.width
if newX > maximumWidth {
if x == 0.0 {
// We are at the beginning of the line and a single label is simply too large. We will need to reduce it's width
label.frame = CGRect(x: x, y: y, width: maximumWidth, height: label.frame.height)
} else {
// We should go into new line
x = 0.0
y += lineHeight
label.frame = CGRect(x: x, y: y, width: min(label.frame.width, maximumWidth), height: label.frame.height)
newX = x + label.frame.width
}
} else {
label.frame = CGRect(x: x, y: y, width: min(label.frame.width, maximumWidth), height: label.frame.height)
}
x = newX + horizontalSeparator
view.addSubview(label)
}
}
This should now let you fill labels the way as on your image. We are also checking for line breaks and labels that can not fit overall width of your view. So now we need to define what is the actual size of your view. y should return that but we need to be able to use it in a table view cell. So we can simply return it:
func layoutLabels(_ labels: [UILabel], inView view: UIView, maximumWidth: CGFloat, lineHeight: CGFloat, horizontalSeparator: CGFloat = 4.0, verticalSeparator: CGFloat = 2.0) -> CGFloat {
...
return labels.count > 0 ? y + lineHeight : 0.0
}
Now in your table view cell it might be best to use some constraint outlet:
protocol Tag {
func createLabel() -> UILabel
}
class Cell: UITableViewCell {
#IBOutlet private var tagsPanel: UIView!
#IBOutlet private var tagsPanelHeightConstraint: NSLayoutConstraint?
var tags: [Tag]!
func refresh() {
tagsPanel.subviews.forEach { $0.removeFromSuperview() } // Ugly, needs improvement
let height = layoutLabels(tags.map { $0.createLabel() }, inView: tagsPanel, maximumWidth: tagsPanel.frame.width, lineHeight: 40.0)
tagsPanelHeightConstraint?.constant = height
}
}
Now the problem is that your table view needs to refresh because your table view cell might have resized. You will need to have a reference to your table view and simple call begin/end updates on it.
var tags: [Tag]!
weak var tableView: UITableView? // Assigned in cellForRowAtIndexPath
func refresh() {
tagsPanel.subviews.forEach { $0.removeFromSuperview() } // Ugly, needs improvement
let height = layoutLabels(tags.map { $0.createLabel() }, inView: tagsPanel, maximumWidth: tagsPanel.frame.width, lineHeight: 40.0)
if let tagsPanelHeightConstraint = tagsPanelHeightConstraint, tagsPanelHeightConstraint.constant != height {
tagsPanelHeightConstraint.constant = height
tableView?.beginUpdates()
tableView?.endUpdates()
}
}
Good luck.
For my first challenge using UIScrollView I modified this example to make UIScrollView display not just another background colour but another UIView and UILabel on each page. But I could have just as easily chosen to display objects like UITableView, UIButton or UIImage.
Potentially, UIScrollView could be much more than a giant content view where users scroll from one part to the next, e.g., some pages might have a UIButton that takes a user to a specific page, the same way we use books.
Code Improvements
My question has evolved since I first posted it. Initially the labels piled up on page 1 (as shown below) but this has now been corrected. I also included this extension to make the font larger.
Further improvement ?
As the code evolved I became more aware of other issues e.g. iPhone 5 images (below) appear differently on iPhone 7 where the UILabel is centred but not the UIView. So my next challenge is possibly to learn how to combine UIScrollView with Autolayout. I invite anyone to spot other things that might be wrong.
ViewController.swift (corrected)
import UIKit
class ViewController: UIViewController,UIScrollViewDelegate {
let scrollView = UIScrollView(frame: CGRect(x: 0, y: 0, width: 320, height: 480))
var views = [UIView]()
var lables = [UILabel]()
var colors:[UIColor] = [UIColor.red, UIColor.magenta, UIColor.blue, UIColor.cyan, UIColor.green, UIColor.yellow]
var frame: CGRect = CGRect.zero
var pageControl: UIPageControl = UIPageControl(frame: CGRect(x: 50, y: 500, width: 200, height: 50))
override func viewDidLoad() {
super.viewDidLoad()
initialiseViewsAndLables()
configurePageControl()
scrollView.delegate = self
self.view.addSubview(scrollView)
for index in 0..<colors.count {
frame.origin.x = self.scrollView.frame.size.width * CGFloat(index)
frame.size = self.scrollView.frame.size
self.scrollView.isPagingEnabled = true
views[index].frame = frame
views[index].backgroundColor = colors[Int(index)]
views[index].layer.cornerRadius = 20
views[index].layer.masksToBounds = true
lables[index].frame = frame
lables[index].center = CGPoint(x: (view.frame.midX + frame.origin.x), y: view.frame.midY)
lables[index].text = String(index + 1)
lables[index].defaultFont = UIFont(name: "HelveticaNeue", size: CGFloat(200))
lables[index].textAlignment = .center
lables[index].textColor = .black
let subView1 = views[index]
let subView2 = lables[index]
self.scrollView .addSubview(subView1)
self.scrollView .addSubview(subView2)
}
print(views, lables)
self.scrollView.contentSize = CGSize(width: self.scrollView.frame.size.width * CGFloat(colors.count), height: self.scrollView.frame.size.height)
pageControl.addTarget(self, action: Selector(("changePage:")), for: UIControlEvents.valueChanged)
}
func initialiseViewsAndLables() {
// Size of views[] and lables[] is linked to available colors
for index in 0..<colors.count {
views.insert(UIView(), at:index)
lables.insert(UILabel(), at: index)
}
}
func configurePageControl() {
// Total number of available pages is based on available colors
self.pageControl.numberOfPages = colors.count
self.pageControl.currentPage = 0
self.pageControl.backgroundColor = getColour()
self.pageControl.pageIndicatorTintColor = UIColor.black
self.pageControl.currentPageIndicatorTintColor = UIColor.green
self.view.addSubview(pageControl)
}
func getColour() -> UIColor {
let index = colors[pageControl.currentPage]
return (index)
}
func changePage(sender: AnyObject) -> () {
scrollView.setContentOffset(CGPoint(x: CGFloat(pageControl.currentPage) * scrollView.frame.size.width, y: 0), animated: true)
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let pageNumber = round(scrollView.contentOffset.x / scrollView.frame.size.width)
pageControl.currentPage = Int(pageNumber)
pageControl.backgroundColor = getColour()
}
}
Extension
extension UILabel{
var defaultFont: UIFont? {
get { return self.font }
set { self.font = newValue }
}
}
The centre point of the lable on each frame must be offset by the origin of the content view (as Baglan pointed out). I've modified the following line of code accordingly.
lables[Int(index)].center = CGPoint(x: (view.frame.midX + frame.origin.x), y: view.frame.midY)
My view hierarchy looks like the following, I have a UITableViewController -> Static Cells -> UITableViewCell -> Custom UIView -> UILabel
My goal is to show circular profile image views and the last view shows a count with the number of remaining images.
That's how I create a circular view which works perfectly fine
private func getCircularViewForPoint(point: CGPoint) -> UIView {
var circularView: UIView = UIView(frame: CGRect(x: point.x, y: point.y, width: 30, height: 30))
circularView.layer.cornerRadius = 15
circularView.layer.masksToBounds = true
circularView.backgroundColor = UIColor.blueColor()
return circularView
}
So now I want to create such a view with a UILabel inside
private func getCircularCountViewForPoint(point: CGPoint, maxAmount: Int) -> UIView {
var circularView = self.getCircularViewForPoint(point)
circularView.backgroundColor = UIColor.brownColor()
var label: UILabel = UILabel(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
label.center = circularView.center
label.text = "XY"
label.font = UIFont.systemFontOfSize(10.0)
label.textAlignment = NSTextAlignment.Center
// self.addSubview(label) // This works but label is now behind circularView of course
circularView.addSubview(label)
return circularView
}
The outcome looks like this, with no UILabel in the brown view.
Frame of circularView:
<UIView: 0x7feb28fd9d50; frame = (295 24.75; 30 30); clipsToBounds = YES; ...
Frame of label:
<UILabel: 0x7feb28ce33b0; frame = (295 24.75; 30 30); userInteractionEnabled = NO; ...
The weird thing is, if I put this code into a playground, it works like expected and the label is visible.
Just for completeness that's how I call these two functions
for var i = 0; i < maxAmount-1; i++ {
self.addSubview(self.getCircularViewForPoint(CGPoint(x: xPos, y: yPos)))
xPos += size+offset
}
// add count view
var countView = self.getCircularCountViewForPoint(CGPoint(x: xPos, y: yPos), maxAmount: maxAmount)
self.addSubview(countView)
since your goal is to show profile pictures i would have tried to replace the brown background with the picture:
circularView.backgroundColor = UIColor(patternImage: UIImage(named: "profilepicturename.png")!)
sorry i couldn't help with the label thing
I'm looking for examples/tutorials/framework explaining how to do a navigation bar/controller which slide to left and right like Tinder.app and Twitter.app
I'm not talking about the faces swiping thing of Tinder, I'm talking about the top menu and the views we can slide entirely to left or right to go smoothly to other screens of the app like profile, moments, etc
I'm looking around but not find anything really interesting until then, I hope you can point me out something.
I'm afraid that the complete solution to this is quite a bit beyond the scope of a single question.
However in the interest of trying to help you I think it's worth looking into this - That's a link to Cocoa Controls, a website which people build ready to go controls you can just drop into your app. (it's quite a cool site really).
That particular link is to MSSlidingPanelController. Which I think is exactly what you are looking for. The source code is clearly visible so you can see exactly what's required to get the effect you are looking for.
Here are a few other examples. Hope this helps.
MSSlidingPanelController is not what you are looking for. These are "drawer views", which only allows user to swipe to a certain drawer.
TwitterPagingViewer and SwiftPagingNav is exactly like the one on Twitter, only more complicated.
Tinder seems to be using a UIPageViewController with hidden dots, which is done by deleting these methods:
presentationCountForPageViewController
presentationIndexForPageViewController
Here is a good tutorial:
https://www.youtube.com/watch?v=8bltsDG2ENQ
Here is a great repo:
https://github.com/goktugyil/EZSwipeController
If you need it in Swift, I've created this one
(it also works on any screen resolution vs just iPhone 4/5/5s like the other example)
https://github.com/aubrey/SwiftPagingNav
class PageViewController: UIViewController, UIScrollViewDelegate {
var scrollView:UIScrollView!
var pageControl:UIPageControl!
var navbarView:UIView!
var navTitleLabel1:UILabel!
var navTitleLabel2:UILabel!
var navTitleLabel3:UILabel!
var view1:UIView!
var view2:UIView!
var view3:UIView!
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor.lightGrayColor()
//Creating some shorthand for these values
var wBounds = self.view.bounds.width
var hBounds = self.view.bounds.height
// This houses all of the UIViews / content
scrollView = UIScrollView()
scrollView.backgroundColor = UIColor.clearColor()
scrollView.frame = self.view.frame
scrollView.pagingEnabled = true
scrollView.showsHorizontalScrollIndicator = false
scrollView.delegate = self
scrollView.bounces = false
self.view.addSubview(scrollView)
self.scrollView.contentSize = CGSize(width: self.view.bounds.size.width * 3, height: hBounds/2)
//Putting a subview in the navigationbar to hold the titles and page dots
navbarView = UIView()
self.navigationController?.navigationBar.addSubview(navbarView)
//Paging control is added to a subview in the uinavigationcontroller
pageControl = UIPageControl()
pageControl.frame = CGRect(x: 0, y: 35, width: 0, height: 0)
pageControl.backgroundColor = UIColor.whiteColor()
pageControl.numberOfPages = 3
pageControl.currentPage = 0
pageControl.currentPageIndicatorTintColor = UIColor(red:0.325, green:0.667, blue:0.922, alpha: 1)
pageControl.pageIndicatorTintColor = UIColor.whiteColor()
self.navbarView.addSubview(pageControl)
//Titles for the nav controller (also added to a subview in the uinavigationcontroller)
//Setting size for the titles. FYI changing width will break the paging fades/movement
var titleSize = CGRect(x: 0, y: 8, width: wBounds, height: 20)
navTitleLabel1 = UILabel()
navTitleLabel1.frame = titleSize
navTitleLabel1.text = "Home"
navTitleLabel1.textAlignment = NSTextAlignment.Center
self.navbarView.addSubview(navTitleLabel1)
navTitleLabel2 = UILabel()
navTitleLabel2.frame = titleSize
navTitleLabel2.text = "Discover"
navTitleLabel2.textAlignment = NSTextAlignment.Center
self.navbarView.addSubview(navTitleLabel2)
navTitleLabel3 = UILabel()
navTitleLabel3.frame = titleSize
navTitleLabel3.text = "Activity"
navTitleLabel3.textAlignment = NSTextAlignment.Center
self.navbarView.addSubview(navTitleLabel3)
//Views for the scrolling view
//This is where the content of your views goes (or you can subclass these and add them to ScrollView)
view1 = UIView()
view1.backgroundColor = UIColor(red:0.325, green:0.667, blue:0.922, alpha: 1)
view1.frame = CGRectMake(0, 0, wBounds, hBounds)
self.scrollView.addSubview(view1)
self.scrollView.bringSubviewToFront(view1)
//Notice the x position increases per number of views
view2 = UIView()
view2.backgroundColor = UIColor(red:0.231, green:0.529, blue:0.757, alpha: 1)
view2.frame = CGRectMake(wBounds, 0, wBounds, hBounds)
self.scrollView.addSubview(view2)
self.scrollView.bringSubviewToFront(view2)
//Notice the x position increases yet again (wBounds * 2)
view3 = UIView()
view3.backgroundColor = UIColor(red:0.529, green:0.600, blue:0.647, alpha: 1)
view3.frame = CGRectMake(wBounds * 2, 0, wBounds, hBounds)
self.scrollView.addSubview(view3)
self.scrollView.bringSubviewToFront(view3)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
navbarView.frame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: 44)
}
func scrollViewDidScroll(scrollView: UIScrollView) {
var xOffset: CGFloat = scrollView.contentOffset.x
//Setup some math to position the elements where we need them when the view is scrolled
var wBounds = self.view.bounds.width
var hBounds = self.view.bounds.height
var widthOffset = wBounds / 100
var offsetPosition = 0 - xOffset/widthOffset
//Apply the positioning values created above to the frame's position based on user's scroll
navTitleLabel1.frame = CGRectMake(offsetPosition, 8, wBounds, 20)
navTitleLabel2.frame = CGRectMake(offsetPosition + 100, 8, wBounds, 20)
navTitleLabel3.frame = CGRectMake(offsetPosition + 200, 8, wBounds, 20)
//Change the alpha values of the titles as they are scrolled
navTitleLabel1.alpha = 1 - xOffset / wBounds
if (xOffset <= wBounds) {
navTitleLabel2.alpha = xOffset / wBounds
} else {
navTitleLabel2.alpha = 1 - (xOffset - wBounds) / wBounds
}
navTitleLabel3.alpha = (xOffset - wBounds) / wBounds
}
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
var xOffset: CGFloat = scrollView.contentOffset.x
//Change the pageControl dots depending on the page / offset values
if (xOffset < 1.0) {
pageControl.currentPage = 0
} else if (xOffset < self.view.bounds.width + 1) {
pageControl.currentPage = 1
} else {
pageControl.currentPage = 2
}
}
}