I am trying to set up a scroll view with auto layout in my storyboard and populate it with a couple of buttons in code, but the scroll view doesn't scroll and I don't understand how to set up the constraints to make scrolling available?
Especially how to set the constraints to the content view (what's that?).
In storyboard the scroll view is placed at the bottom of the screen (20 pix to the safe area) and from leading to trailing. It has a size of 375x80.
Now in code this is how the scroll view is populated with buttons and how it is set up:
override func viewDidLoad() {
super.viewDidLoad()
var xCoord: CGFloat = 5
var yCoord: CGFloat = 5
let buttonWidth: CGFloat = 70
let buttonHeight: CGFloat = 70
let gapBetweenButtons: CGFloat = 5
var itemCount = 0
for i in 0..<CIFilterNames.count {
itemCount = 1
// Button properties
let filterButton = UIButton(type: .custom)
filterButton.frame = CGRect(x: xCoord, y: yCoord, width: buttonWidth, height: buttonHeight)
filterButton.tag = itemCount
filterButton.addTarget(self, action: #selector(ViewController.filterButtonTapped(sender:)), for: .touchUpInside)
filterButton.layer.cornerRadius = 6
filterButton.clipsToBounds = true
//Code for filters will be added here
let ciContext = CIContext(options: nil)
let coreImage = CIImage(image: originalImage.image!)
let filter = CIFilter(name: "\(CIFilterNames[i])")
filter!.setDefaults()
filter?.setValue(coreImage, forKey: kCIInputImageKey)
let filteredImageDate = filter!.value(forKey: kCIOutputImageKey) as! CIImage
let filteredImageRef = ciContext.createCGImage(filteredImageDate, from: filteredImageDate.extent)
let imageForButton = UIImage(cgImage: filteredImageRef!)
// Asign filtered image to the button
filterButton.setBackgroundImage(imageForButton, for: .normal)
// Add buttons in the scrollView
xCoord += buttonWidth + gapBetweenButtons
filterScrollView.addSubview(filterButton)
}
filterScrollView.contentSize = CGSize(width: buttonWidth * CGFloat(itemCount + 2), height: yCoord)
filterScrollView.isScrollEnabled = true
}
Depending on the device size 4 or 5 buttons are shown, but not more and scrolling is not possible.
What can be done to make scrolling possible?
Reason is in itemCount , you're settings it to itemCount + 2 will be 3 as itemCount is 1 from the for loop so , set it to equal array size
filterScrollView.contentSize = CGSize(width: buttonWidth * CIFilterNames.count , height: yCoord)
You will be much better off using auto-layout over calculating sizes - gives much more flexibility for future changes.
Here is an example you can run in a Playground page. I used the "count" as the button labels, since I don't have your images. If you add images, you can un-comment the appropriate lines and remove the .setTitle and .backgroundColor lines:
import UIKit
import PlaygroundSupport
import CoreImage
class TestViewController : UIViewController {
var CIFilterNames = [
"CIPhotoEffectChrome",
"CIPhotoEffectFade",
"CIPhotoEffectInstant",
"CIPhotoEffectNoir",
"CIPhotoEffectProcess",
"CIPhotoEffectTonal",
"CIPhotoEffectTransfer",
"CISepiaTone"
]
let buttonWidth: CGFloat = 70
let buttonHeight: CGFloat = 70
let gapBetweenButtons: CGFloat = 5
override func viewDidLoad() {
super.viewDidLoad()
// create a UIScrollView
let filterScrollView = UIScrollView()
// we will set the auto-layout constraints
filterScrollView.translatesAutoresizingMaskIntoConstraints = false
// set background color so we can see the scrollView when the images are scrolled
filterScrollView.backgroundColor = .orange
// add the scrollView to the view
view.addSubview(filterScrollView)
// pin scrollView 20-pts from bottom/leading/trailing
filterScrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20.0).isActive = true
filterScrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20.0).isActive = true
filterScrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -20.0).isActive = true
// scrollView height is 80
filterScrollView.heightAnchor.constraint(equalToConstant: 80).isActive = true
// create a UIStackView
let stackView = UIStackView()
stackView.spacing = gapBetweenButtons
// we will set the auto-layout constraints
stackView.translatesAutoresizingMaskIntoConstraints = false
// add the stackView to the scrollView
filterScrollView.addSubview(stackView)
// with auto-layout, scroll views use the content's constraints to
// determine the contentSize,
// so pin the stackView to top/bottom/leading/trailing of the scrollView
// with padding to match the gapBetweenButtons
stackView.leadingAnchor.constraint(equalTo: filterScrollView.leadingAnchor, constant: gapBetweenButtons).isActive = true
stackView.topAnchor.constraint(equalTo: filterScrollView.topAnchor, constant: gapBetweenButtons).isActive = true
stackView.trailingAnchor.constraint(equalTo: filterScrollView.trailingAnchor, constant: -gapBetweenButtons).isActive = true
stackView.bottomAnchor.constraint(equalTo: filterScrollView.bottomAnchor, constant: -gapBetweenButtons).isActive = true
// loop through the images
for i in 0..<CIFilterNames.count {
// create a new UIButton
let filterButton = UIButton(type: .custom)
filterButton.tag = i
// we will set the auto-layout constraints, and allow the stackView
// to handle the placement
filterButton.translatesAutoresizingMaskIntoConstraints = false
// set the width and height constraints
filterButton.widthAnchor.constraint(equalToConstant: buttonWidth).isActive = true
filterButton.heightAnchor.constraint(equalToConstant: buttonHeight).isActive = true
filterButton.layer.cornerRadius = 6
filterButton.clipsToBounds = true
filterButton.addTarget(self, action: #selector(filterButtonTapped(_:)), for: .touchUpInside)
// this example doesn't have your images, so
// set button title and background color
filterButton.setTitle("\(i)", for: .normal)
filterButton.backgroundColor = .blue
// //Code for filters will be added here
//
// let ciContext = CIContext(options: nil)
// let coreImage = CIImage(image: originalImage.image!)
// let filter = CIFilter(name: "\(CIFilterNames[i])")
// filter!.setDefaults()
// filter?.setValue(coreImage, forKey: kCIInputImageKey)
// let filteredImageDate = filter!.value(forKey: kCIOutputImageKey) as! CIImage
// let filteredImageRef = ciContext.createCGImage(filteredImageDate, from: filteredImageDate.extent)
// let imageForButton = UIImage(cgImage: filteredImageRef!)
//
// // Asign filtered image to the button
// filterButton.setBackgroundImage(imageForButton, for: .normal)
// add the image view to the stackView
stackView.addArrangedSubview(filterButton)
}
}
func filterButtonTapped(_ sender: Any?) -> Void {
if let b = sender as? UIButton {
print("Tapped:", b.tag)
}
}
}
let vc = TestViewController()
vc.view.backgroundColor = .red
PlaygroundPage.current.liveView = vc
Related
My question and code is based on this answer to one of my previous questions. I have programmatically created stackview where several labels are stored and I'm trying to make these labels clickable. I tried two different solutions:
Make clickable label. I created function and assigned it to the label in the gesture recognizer:
public func setTapListener(_ label: UILabel){
let tapGesture: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapGestureMethod(_:)))
tapGesture.numberOfTapsRequired = 1
tapGesture.numberOfTouchesRequired = 1
label.isUserInteractionEnabled = true
label.addGestureRecognizer(tapGesture)
}
#objc func tapGestureMethod(_ gesture: UITapGestureRecognizer) {
print(gesture.view?.tag)
}
but it does not work. Then below the second way....
I thought that maybe the 1st way does not work because the labels are in UIStackView so I decided to assign click listener to the stack view and then determine on which view we clicked. At first I assigned to each of labels in the stackview tag and listened to clicks:
let tap = UITapGestureRecognizer(target: self, action: #selector(didTapCard(sender:)))
labelsStack.addGestureRecognizer(tap)
....
#objc func didTapCard (sender: UITapGestureRecognizer) {
(sender.view as? UIStackView)?.arrangedSubviews.forEach({ label in
print((label as! UILabel).text)
})
}
but the problem is that the click listener works only on the part of the stack view and when I tried to determine on which view we clicked it was not possible.
I think that possibly the problem is with that I tried to assign one click listener to several views, but not sure that works as I thought. I'm trying to make each label in the stackview clickable, but after click I will only need getting text from the label, so that is why I used one click listener for all views.
Applying a transform to a view (button, label, view, etc) changes the visual appearance, not the structure.
Because you're working with rotated views, you need to implement hit-testing.
Quick example:
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// convert the point to the labels stack view coordinate space
let pt = labelsStack.convert(point, from: self)
// loop through arranged subviews
for i in 0..<labelsStack.arrangedSubviews.count {
let v = labelsStack.arrangedSubviews[i]
// if converted point is inside subview
if v.frame.contains(pt) {
return v
}
}
return super.hitTest(point, with: event)
}
Assuming you're still working with the MyCustomView class and layout from your previous questions, we'll build on that with a few changes for layout, and to allow tapping the labels.
Complete example:
class Step5VC: UIViewController {
// create the custom "left-side" view
let myView = MyCustomView()
// create the "main" stack view
let mainStackView = UIStackView()
// create the "bottom labels" stack view
let bottomLabelsStack = UIStackView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemYellow
guard let img = UIImage(named: "pro1") else {
fatalError("Need an image!")
}
// create the image view
let imgView = UIImageView()
imgView.contentMode = .scaleToFill
imgView.image = img
mainStackView.axis = .horizontal
bottomLabelsStack.axis = .horizontal
bottomLabelsStack.distribution = .fillEqually
// add views to the main stack view
mainStackView.addArrangedSubview(myView)
mainStackView.addArrangedSubview(imgView)
// add main stack view and bottom labels stack view to view
mainStackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(mainStackView)
bottomLabelsStack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(bottomLabelsStack)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain Top/Leading/Trailing
mainStackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
mainStackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
//mainStackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
// we want the image view to be 270 x 270
imgView.widthAnchor.constraint(equalToConstant: 270.0),
imgView.heightAnchor.constraint(equalTo: imgView.widthAnchor),
// constrain the bottom lables to the bottom of the main stack view
// same width as the image view
// aligned trailing
bottomLabelsStack.topAnchor.constraint(equalTo: mainStackView.bottomAnchor),
bottomLabelsStack.trailingAnchor.constraint(equalTo: mainStackView.trailingAnchor),
bottomLabelsStack.widthAnchor.constraint(equalTo: imgView.widthAnchor),
])
// setup the left-side custom view
myView.titleText = "Gefährdung"
let titles: [String] = [
"keine / gering", "mittlere", "erhöhte", "hohe",
]
let colors: [UIColor] = [
UIColor(red: 0.863, green: 0.894, blue: 0.527, alpha: 1.0),
UIColor(red: 0.942, green: 0.956, blue: 0.767, alpha: 1.0),
UIColor(red: 0.728, green: 0.828, blue: 0.838, alpha: 1.0),
UIColor(red: 0.499, green: 0.706, blue: 0.739, alpha: 1.0),
]
for (c, t) in zip(colors, titles) {
// because we'll be using hitTest in our Custom View
// we don't need to set .isUserInteractionEnabled = true
// create a "color label"
let cl = colorLabel(withColor: c, title: t, titleColor: .black)
// we're limiting the height to 270, so
// let's use a smaller font for the left-side labels
cl.font = .systemFont(ofSize: 12.0, weight: .light)
// create a tap recognizer
let t = UITapGestureRecognizer(target: self, action: #selector(didTapRotatedLeftLabel(_:)))
// add the recognizer to the label
cl.addGestureRecognizer(t)
// add the label to the custom myView
myView.addLabel(cl)
}
// rotate the left-side custom view 90-degrees counter-clockwise
myView.rotateTo(-.pi * 0.5)
// setup the bottom labels
let colorDictionary = [
"Red":UIColor.systemRed,
"Green":UIColor.systemGreen,
"Blue":UIColor.systemBlue,
]
for (myKey,myValue) in colorDictionary {
// bottom labels are not rotated, so we can add tap gesture recognizer directly
// create a "color label"
let cl = colorLabel(withColor: myValue, title: myKey, titleColor: .white)
// let's use a smaller, bold font for the left-side labels
cl.font = .systemFont(ofSize: 12.0, weight: .bold)
// by default, .isUserInteractionEnabled is False for UILabel
// so we must set .isUserInteractionEnabled = true
cl.isUserInteractionEnabled = true
// create a tap recognizer
let t = UITapGestureRecognizer(target: self, action: #selector(didTapBottomLabel(_:)))
// add the recognizer to the label
cl.addGestureRecognizer(t)
bottomLabelsStack.addArrangedSubview(cl)
}
}
#objc func didTapRotatedLeftLabel (_ sender: UITapGestureRecognizer) {
if let v = sender.view as? UILabel {
let title = v.text ?? "label with no text"
print("Tapped Label in Rotated Custom View:", title)
// do something based on the tapped label/view
}
}
#objc func didTapBottomLabel (_ sender: UITapGestureRecognizer) {
if let v = sender.view as? UILabel {
let title = v.text ?? "label with no text"
print("Tapped Bottom Label:", title)
// do something based on the tapped label/view
}
}
func colorLabel(withColor color:UIColor, title:String, titleColor:UIColor) -> UILabel {
let newLabel = PaddedLabel()
newLabel.padding = UIEdgeInsets(top: 6, left: 8, bottom: 6, right: 8)
newLabel.backgroundColor = color
newLabel.text = title
newLabel.textAlignment = .center
newLabel.textColor = titleColor
newLabel.setContentHuggingPriority(.required, for: .vertical)
return newLabel
}
}
class MyCustomView: UIView {
public var titleText: String = "" {
didSet { titleLabel.text = titleText }
}
public func addLabel(_ v: UIView) {
labelsStack.addArrangedSubview(v)
}
public func rotateTo(_ d: Double) {
// get the container view (in this case, it's the outer stack view)
if let v = subviews.first {
// set the rotation transform
if d == 0 {
self.transform = .identity
} else {
self.transform = CGAffineTransform(rotationAngle: d)
}
// remove the container view
v.removeFromSuperview()
// tell it to layout itself
v.setNeedsLayout()
v.layoutIfNeeded()
// get the frame of the container view
// apply the same transform as self
let r = v.frame.applying(self.transform)
wC.isActive = false
hC.isActive = false
// add it back
addSubview(v)
// set self's width and height anchors
// to the width and height of the container
wC = self.widthAnchor.constraint(equalToConstant: r.width)
hC = self.heightAnchor.constraint(equalToConstant: r.height)
guard let sv = v.superview else {
fatalError("no superview")
}
// apply the new constraints
NSLayoutConstraint.activate([
v.centerXAnchor.constraint(equalTo: self.centerXAnchor),
v.centerYAnchor.constraint(equalTo: self.centerYAnchor),
wC,
outerStack.widthAnchor.constraint(equalTo: sv.heightAnchor),
])
}
}
// our subviews
private let outerStack = UIStackView()
private let titleLabel = UILabel()
private let labelsStack = UIStackView()
private var wC: NSLayoutConstraint!
private var hC: NSLayoutConstraint!
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
// stack views and label properties
outerStack.axis = .vertical
outerStack.distribution = .fillEqually
labelsStack.axis = .horizontal
// let's use .fillProportionally to help fit the labels
labelsStack.distribution = .fillProportionally
titleLabel.textAlignment = .center
titleLabel.backgroundColor = .lightGray
titleLabel.textColor = .white
// add title label and labels stack to outer stack
outerStack.addArrangedSubview(titleLabel)
outerStack.addArrangedSubview(labelsStack)
outerStack.translatesAutoresizingMaskIntoConstraints = false
addSubview(outerStack)
wC = self.widthAnchor.constraint(equalTo: outerStack.widthAnchor)
hC = self.heightAnchor.constraint(equalTo: outerStack.heightAnchor)
NSLayoutConstraint.activate([
outerStack.centerXAnchor.constraint(equalTo: self.centerXAnchor),
outerStack.centerYAnchor.constraint(equalTo: self.centerYAnchor),
wC, hC,
])
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// convert the point to the labels stack view coordinate space
let pt = labelsStack.convert(point, from: self)
// loop through arranged subviews
for i in 0..<labelsStack.arrangedSubviews.count {
let v = labelsStack.arrangedSubviews[i]
// if converted point is inside subview
if v.frame.contains(pt) {
return v
}
}
return super.hitTest(point, with: event)
}
}
class PaddedLabel: UILabel {
var padding: UIEdgeInsets = .zero
override func drawText(in rect: CGRect) {
super.drawText(in: rect.inset(by: padding))
}
override var intrinsicContentSize : CGSize {
let sz = super.intrinsicContentSize
return CGSize(width: sz.width + padding.left + padding.right, height: sz.height + padding.top + padding.bottom)
}
}
The problem is with the the stackView's height. Once the label is rotated, the stackview's height is same as before and the tap gestures will only work within stackview's bounds.
I have checked it by changing the height of the stackview at the transform and observed tap gestures are working fine with the rotated label but with the part of it inside the stackview.
Now the problem is that you have to keep the bounds of the label inside the stackview either by changing it axis(again a new problem as need to handle the layout with it) or you have to handle it without the stackview.
You can check the observation by clicking the part of rotated label inside stackview and outside stackview.
Code to check it:
class ViewController: UIViewController {
var centerLabel = UILabel()
let mainStackView = UIStackView()
var stackViewHeightCons:NSLayoutConstraint?
var stackViewTopsCons:NSLayoutConstraint?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemYellow
mainStackView.axis = .horizontal
mainStackView.alignment = .top
mainStackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(mainStackView)
mainStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
mainStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
stackViewTopsCons = mainStackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 300)
stackViewTopsCons?.isActive = true
stackViewHeightCons = mainStackView.heightAnchor.constraint(equalToConstant: 30)
stackViewHeightCons?.isActive = true
centerLabel.textAlignment = .center
centerLabel.text = "Let's rotate this label"
centerLabel.backgroundColor = .green
centerLabel.tag = 11
setTapListener(centerLabel)
mainStackView.addArrangedSubview(centerLabel)
// outline the stack view so we can see its frame
mainStackView.layer.borderColor = UIColor.red.cgColor
mainStackView.layer.borderWidth = 1
}
public func setTapListener(_ label: UILabel){
let tapGesture: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapGestureMethod(_:)))
tapGesture.numberOfTapsRequired = 1
tapGesture.numberOfTouchesRequired = 1
label.isUserInteractionEnabled = true
label.addGestureRecognizer(tapGesture)
}
#objc func tapGestureMethod(_ gesture: UITapGestureRecognizer) {
print(gesture.view?.tag ?? 0)
var yCor:CGFloat = 300
if centerLabel.transform == .identity {
centerLabel.transform = CGAffineTransform(rotationAngle: -CGFloat.pi / 2)
yCor = mainStackView.frame.origin.y - (centerLabel.frame.size.height/2)
} else {
centerLabel.transform = .identity
}
updateStackViewHeight(topCons: yCor)
}
private func updateStackViewHeight(topCons:CGFloat) {
stackViewTopsCons?.constant = topCons
stackViewHeightCons?.constant = centerLabel.frame.size.height
}
}
Sorry. My assumption was incorrect.
Why are you decided to use Label instead of UIButton (with transparence background color and border line)?
Also you can use UITableView instead of stack & labels
Maybe this documentation will help too (it is written that usually in one view better to keep one gesture recognizer): https://developer.apple.com/documentation/uikit/touches_presses_and_gestures/coordinating_multiple_gesture_recognizers
I tried save textview as image with not device scale. I implemented a method to save an image by adding an arbitrary textview according to the UI value. Because when I tried save image using drawHierarchy method in up scale, image was blurry.
Condition when textview is saved blurry
not device scale (up scale)
1-1. isScrollEnabled = false and height of textview is more than 128.
1-2. isScrollEnabled = true (always text is blurry)
here is my code
func drawQuoteImage() {
var campusSize = view.frame.size
var scale = UIScreen.main.scale + 2
// 1. Create View
let quoteView = UIView(frame: CGRect(x: 0, y: 0, width: campusSize.width, height: campusSize.height))
let textview = UITextView()
textview.attributedText = NSAttributedString(string: quoteLabel.text, attributes: textAttributes as [NSAttributedString.Key : Any])
textview.frame = transfromFrame(originalFrame: quoteLabel.frame, campusSize: campusSize)
quoteView.addSubview(textview)
// 2. Render image
UIGraphicsBeginImageContextWithOptions(quoteView.frame.size, false, scale)
let context = UIGraphicsGetCurrentContext()!
context.setRenderingIntent(.relativeColorimetric)
context.interpolationQuality = .high
quoteView.drawHierarchy(in: quoteView.frame, afterScreenUpdates: true)
quoteView.layer.render(in: context)
let image = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
quoteImage = image
}
private func transfromFrame(originalFrame: CGRect, campusSize: CGSize) -> CGRect
{
if UIDevice.current.screenType == .iPhones_X_XS {
return CGRect(x: round(originalFrame.origin.x), y: round(originalFrame.origin.y), width: round(originalFrame.width), height: round(originalFrame.height))
}
else {
var frame = CGRect()
let ratioBasedOnWidth = campusSize.width / editView.frame.width
let ratioBasedOnHeight = campusSize.height / editView.frame.height
frame.size.width = round(originalFrame.width * ratioBasedOnWidth)
frame.size.height = round(originalFrame.height * ratioBasedOnHeight)
frame.origin.x = round(originalFrame.origin.x * ratioBasedOnWidth)
frame.origin.y = round(originalFrame.origin.y * ratioBasedOnHeight)
return frame
}
}
Wired Point
when height of textview is more than 128, textview is save blurry. I found related value when I put textview default height is 128.
Height is 128 or less (when isScrollEnabled is false), textview is saved always clear. But when height is more than 128, it looks blurry.
Height 128
Height 129
I'd like to know how to clearly draw image with textview at #5x scale. (textview height is bigger than 128)
Here's a quick example using a UIView extension from this accepted answer: https://stackoverflow.com/a/51944513/6257435
We'll create a UITextView with a size of 240 x 129. Then add 4 buttons to capture the text view at 1x, 2x, 5x and 10x scale.
It looks like this when running:
and the result...
At 1x scale - 240 x 129 pixels:
At 2x scale - 480 x 258 pixels:
At 5x scale - 1200 x 645 pixels (just showing a portion):
At 10x scale - 2400 x 1290 pixels (just showing a portion):
The extension:
extension UIView {
func scale(by scale: CGFloat) {
self.contentScaleFactor = scale
for subview in self.subviews {
subview.scale(by: scale)
}
}
func getImage(scale: CGFloat? = nil) -> UIImage {
let newScale = scale ?? UIScreen.main.scale
self.scale(by: newScale)
let format = UIGraphicsImageRendererFormat()
format.scale = newScale
let renderer = UIGraphicsImageRenderer(size: self.bounds.size, format: format)
let image = renderer.image { rendererContext in
self.layer.render(in: rendererContext.cgContext)
}
return image
}
}
Sample controller code:
class TextViewCapVC: UIViewController {
let textView = UITextView()
let resultLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
// add a stack view with buttons
let stack = UIStackView()
stack.axis = .vertical
stack.spacing = 12
[1, 2, 5, 10].forEach { i in
let btn = UIButton()
btn.setTitle("Create Image at \(i)x scale", for: [])
btn.setTitleColor(.white, for: .normal)
btn.setTitleColor(.lightGray, for: .highlighted)
btn.backgroundColor = .systemBlue
btn.tag = i
btn.addTarget(self, action: #selector(gotTap(_:)), for: .touchUpInside)
stack.addArrangedSubview(btn)
}
[textView, stack, resultLabel].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// text view 280x240, 20-points from top, centered horizontally
textView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
textView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
textView.widthAnchor.constraint(equalToConstant: 240.0),
textView.heightAnchor.constraint(equalToConstant: 129.0),
// stack view, 20-points from text view, same width, centered horizontally
stack.topAnchor.constraint(equalTo: textView.bottomAnchor, constant: 20.0),
stack.centerXAnchor.constraint(equalTo: g.centerXAnchor),
stack.widthAnchor.constraint(equalTo: textView.widthAnchor),
// result label, 20-points from stack view
// 20-points from leading/trailing
resultLabel.topAnchor.constraint(equalTo: stack.bottomAnchor, constant: 20.0),
resultLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
resultLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
])
let string = "Test"
let attributes: [NSAttributedString.Key: Any] = [
.foregroundColor: UIColor.blue,
.font: UIFont.italicSystemFont(ofSize: 104.0),
]
let attributedString = NSMutableAttributedString(string: string, attributes: attributes)
textView.attributedText = attributedString
resultLabel.font = .systemFont(ofSize: 14, weight: .light)
resultLabel.numberOfLines = 0
resultLabel.text = "Results:"
// so we can see the view frames
textView.backgroundColor = .yellow
resultLabel.backgroundColor = .cyan
}
#objc func gotTap(_ sender: Any?) {
guard let btn = sender as? UIButton else { return }
let scaleFactor = CGFloat(btn.tag)
let img = textView.getImage(scale: scaleFactor)
var s: String = "Results:\n\n"
let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let fName: String = "\(btn.tag)xScale-\(img.size.width * img.scale)x\(img.size.height * img.scale).png"
let url = documents.appendingPathComponent(fName)
if let data = img.pngData() {
do {
try data.write(to: url)
} catch {
s += "Unable to Write Image Data to Disk"
resultLabel.text = s
return
}
} else {
s += "Could not get png data"
resultLabel.text = s
return
}
s += "Logical Size: \(img.size)\n\n"
s += "Scale: \(img.scale)\n\n"
s += "Pixel Size: \(CGSize(width: img.size.width * img.scale, height: img.size.height * img.scale))\n\n"
s += "File \"\(fName)\"\n\nsaved to Documents folder\n"
resultLabel.text = s
// print the path to documents in debug console
// so we can copy/paste into Finder to get to the files
print(documents.path)
}
}
I'm trying to get my views to animate from top to bottom. Currently, when changing the text of my label, between nil and some "error message", the labels are animated from the center of its intrinsic size, but I want the regular "label" to be "static" and only animate the errorlabel. Basically the error label should be located directly below the regular label and the errorlabel should be expanded according to its (intrinsic)height. This is essentially for a checkbox. I want to show the error message when the user hasn't checked the checkbox yet, but are trying to proceed further. The code is just a basic implementation that explains the problem. I've tried adjusting anchorPoint and contentMode for the containerview but those doesn't seem to work the way I thought. Sorry if the indentation is weird
import UIKit
class ViewController: UIViewController {
let container = UIView()
let errorLabel = UILabel()
var bottomLabel: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(container)
container.contentMode = .top
container.translatesAutoresizingMaskIntoConstraints = false
container.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
container.bottomAnchor.constraint(lessThanOrEqualTo: view.bottomAnchor).isActive = true
container.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
container.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
let label = UILabel()
label.text = "Very long text that i would like to show to full extent and eventually add an error message to. It'll work on multiple rows obviously"
label.numberOfLines = 0
container.contentMode = .top
container.addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
label.topAnchor.constraint(equalTo: container.topAnchor).isActive = true
label.bottomAnchor.constraint(lessThanOrEqualTo: container.bottomAnchor).isActive = true
label.leadingAnchor.constraint(equalTo: container.leadingAnchor).isActive = true
label.trailingAnchor.constraint(equalTo: container.trailingAnchor).isActive = true
container.addSubview(errorLabel)
errorLabel.setContentHuggingPriority(UILayoutPriority(300), for: .vertical)
errorLabel.translatesAutoresizingMaskIntoConstraints = false
errorLabel.topAnchor.constraint(equalTo: label.bottomAnchor).isActive = true
errorLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor).isActive = true
errorLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor).isActive = true
bottomLabel = errorLabel.bottomAnchor.constraint(lessThanOrEqualTo: container.bottomAnchor)
bottomLabel.isActive = false
errorLabel.numberOfLines = 0
container.backgroundColor = .green
let tapRecognizer = UITapGestureRecognizer()
tapRecognizer.addTarget(self, action: #selector(onTap))
container.addGestureRecognizer(tapRecognizer)
}
#objc func onTap() {
self.container.layoutIfNeeded()
UIView.animate(withDuration: 0.3, animations: {
let active = !self.bottomLabel.isActive
self.bottomLabel.isActive = active
self.errorLabel.text = active ? "A veru very veru very veru very veru very veru very veru very veru very veru very long Error message" : nil
self.container.layoutIfNeeded()
})
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
I have found it a bit difficult to get dynamic multiline labels to "animate" in the way I want - particularly when I want to "hide" the label.
One approach: Create 2 "error" labels, with one overlaid on top of the other. Use the "hidden" label to control the constraints on the container view. When animating the change, the container view's bounds will effectively "reveal" and "conceal" (show/hide) the "visible" label.
Here is an example, that you can run directly in a Playground page:
import UIKit
import PlaygroundSupport
class RevealViewController: UIViewController {
let container = UIView()
let staticLabel = UILabel()
let hiddenErrorLabel = UILabel()
let visibleErrorLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
// colors, just so we can see the bounds of the labels
view.backgroundColor = .lightGray
container.backgroundColor = .green
staticLabel.backgroundColor = .yellow
visibleErrorLabel.backgroundColor = .cyan
// we don't want to see this label, so set its alpha to zero
hiddenErrorLabel.alpha = 0.0
// we want the Error Label to be "revealed" - so when it is has text it is initially "covered"
container.clipsToBounds = true
// all labels may be multiple lines
staticLabel.numberOfLines = 0
hiddenErrorLabel.numberOfLines = 0
visibleErrorLabel.numberOfLines = 0
// initial text in the "static" label
staticLabel.text = "Very long text that i would like to show to full extent and eventually add an error message to. It'll work on multiple rows obviously"
// add the container view to the VC's view
// pin it to the sides, and 100-pts from the top
// NO bottom constraint
view.addSubview(container)
container.translatesAutoresizingMaskIntoConstraints = false
container.topAnchor.constraint(equalTo: view.topAnchor, constant: 100.0).isActive = true
container.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
container.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
// add the static label to the container
// pin it to the top and sides
// NO bottom constraint
container.addSubview(staticLabel)
staticLabel.translatesAutoresizingMaskIntoConstraints = false
staticLabel.topAnchor.constraint(equalTo: container.topAnchor).isActive = true
staticLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor).isActive = true
staticLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor).isActive = true
// add the "hidden" error label to the container
// pin it to the sides, and pin its top to the bottom of the static label
// NO bottom constraint
container.addSubview(hiddenErrorLabel)
hiddenErrorLabel.translatesAutoresizingMaskIntoConstraints = false
hiddenErrorLabel.topAnchor.constraint(equalTo: staticLabel.bottomAnchor).isActive = true
hiddenErrorLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor).isActive = true
hiddenErrorLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor).isActive = true
// add the "visible" error label to the container
// pin its top, leading and trailing constraints to the hidden label
container.addSubview(visibleErrorLabel)
visibleErrorLabel.translatesAutoresizingMaskIntoConstraints = false
visibleErrorLabel.topAnchor.constraint(equalTo: hiddenErrorLabel.topAnchor).isActive = true
visibleErrorLabel.leadingAnchor.constraint(equalTo: hiddenErrorLabel.leadingAnchor).isActive = true
visibleErrorLabel.trailingAnchor.constraint(equalTo: hiddenErrorLabel.trailingAnchor).isActive = true
// pin the bottom of the hidden label ot the bottom of the container
// now, when we change the text of the hidden label, it will
// "push down / pull up" the bottom of the container view
hiddenErrorLabel.bottomAnchor.constraint(equalTo: container.bottomAnchor).isActive = true
// add a tap gesture
let tapRecognizer = UITapGestureRecognizer()
tapRecognizer.addTarget(self, action: #selector(onTap))
container.addGestureRecognizer(tapRecognizer)
}
var myActive = false
#objc func onTap() {
let errorText = "A veru very veru very veru very veru very veru very veru very veru very veru very long Error message"
self.myActive = !self.myActive
if self.myActive {
// we want to SHOW the error message
// set the error message in the VISIBLE error label
self.visibleErrorLabel.text = errorText
// "animate" it, with duration of 0.0 - so it is filled instantly
// it will extend below the bottom of the container view, but won't be
// visible yet because we set .clipsToBounds = true on the container
UIView.animate(withDuration: 0.0, animations: {
}, completion: {
_ in
// now, set the error message in the HIDDEN error label
self.hiddenErrorLabel.text = errorText
// the hidden label will now "push down" the bottom of the container view
// so we can animate the "reveal"
UIView.animate(withDuration: 0.3, animations: {
self.view.layoutIfNeeded()
})
})
} else {
// we want to HIDE the error message
// clear the text from the HIDDEN error label
self.hiddenErrorLabel.text = ""
// the hidden label will now "pull up" the bottom of the container view
// so we can animate the "conceal"
UIView.animate(withDuration: 0.3, animations: {
self.view.layoutIfNeeded()
}, completion: {
_ in
// after its hidden, clear the text of the VISIBLE error label
self.visibleErrorLabel.text = ""
})
}
}
}
let vc = RevealViewController()
PlaygroundPage.current.liveView = vc
So, since it's a control that I wanted to create (checkbox) in this case with an error message, I manipulated the frames directly, based on the bounds. So to get it to work properly, I used a combination of overriding intrinsicContentSize and layoutSubviews and some minor extra stuff. The class contains a bit more than provided, but the provided code should hopefully explain the approach I went with.
open class Checkbox: UIView {
let imageView = UIImageView()
let textView = ThemeableTapLabel()
private let errorLabel = UILabel()
var errorVisible: Bool = false
let checkboxPad: CGFloat = 8
override open var bounds: CGRect {
didSet {
// fixes layout when bounds change
invalidateIntrinsicContentSize()
}
}
open var errorMessage: String? {
didSet {
self.errorVisible = self.errorMessage != nil
UIView.animate(withDuration: 0.3, animations: {
if self.errorMessage != nil {
self.errorLabel.text = self.errorMessage
}
self.setNeedsLayout()
self.invalidateIntrinsicContentSize()
self.layoutIfNeeded()
}, completion: { success in
if self.errorMessage == nil {
self.errorLabel.text = nil
}
})
}
}
func checkboxSize() -> CGSize {
return CGSize(width: imageView.image?.size.width ?? 0, height: imageView.image?.size.height ?? 0)
}
override open func layoutSubviews() {
super.layoutSubviews()
frame = bounds
let imageFrame = CGRect(x: 0, y: 0, width: checkboxSize().width, height: checkboxSize().height)
imageView.frame = imageFrame
let textRect = textView.textRect(forBounds: CGRect(x: (imageFrame.width + checkboxPad), y: 0, width: bounds.width - (imageFrame.width + checkboxPad), height: 10000), limitedToNumberOfLines: textView.numberOfLines)
textView.frame = textRect
let largestHeight = max(checkboxSize().height, textRect.height)
let rect = errorLabel.textRect(forBounds: CGRect(x: 0, y: 0, width: bounds.width, height: 10000), limitedToNumberOfLines: errorLabel.numberOfLines)
//po bourect = rect.offsetBy(dx: 0, dy: imageFrame.maxY)
let errorHeight = errorVisible ? rect.height : 0
errorLabel.frame = CGRect(x: 0, y: largestHeight, width: bounds.width, height: errorHeight)
}
override open var intrinsicContentSize: CGSize {
get {
let textRect = textView.textRect(forBounds: CGRect(x: (checkboxSize().width + checkboxPad), y: 0, width: bounds.width - (checkboxSize().width + checkboxPad), height: 10000), limitedToNumberOfLines: textView.numberOfLines)
let rect = errorLabel.textRect(forBounds: CGRect(x: 0, y: 0, width: bounds.width, height: 10000), limitedToNumberOfLines: errorLabel.numberOfLines)
let errorHeight = errorVisible ? rect.height : 0
let largestHeight = max(checkboxSize().height, textRect.height)
return CGSize(width: checkboxSize().width + 200, height: largestHeight + errorHeight)
}
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
func setup() {
//...
addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
addSubview(textView)
textView.translatesAutoresizingMaskIntoConstraints = false
textView.numberOfLines = 0
contentMode = .top
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(checkboxTap(sender:)))
self.isUserInteractionEnabled = true
self.addGestureRecognizer(tapGesture)
addSubview(errorLabel)
errorLabel.contentMode = .top
errorLabel.textColor = .red
errorLabel.numberOfLines = 0
}
}
I have an imageview called "cardImgView" in that I want to load two images by scrolling horizontally, I have tried the following way, in this case I can able to scroll only to up and down and the images also not changing, anyone
have idea how to do this correctly.
let img: UIImage = self.dataDict.object(forKey: kCardImgFront) as! UIImage
let img2:UIImage = self.dataDict.object(forKey: kCardImgBack) as! UIImage
imgArray = [img, img2]
for i in 0..<imgArray.count{
cardImgView?.image = imgArray[i]
scrollView.contentSize.width = scrollView.frame.width * CGFloat(i + 1)
scrollView.addSubview(cardImgView!)
}
thanks in advance.
First, as I commented, you are currently using a single UIImageView --- so each time through your for-loop you are just replacing the .image of that one image view.
Second, you will be much better off using auto-layout and constraints, instead of trying to explicitly set frames and the scrollView's contentSize.
Third, UIStackView is ideal for your use case - adding multiple images that you want to horizontally scroll.
So, the general idea is:
add a scroll view
add a stack view to the scroll view
use constraints to make the stack view control the scroll view's contentSize
create a new UIImageView for each image
add each image view to the stack view
Here is a simple example that you can run in a Playground page to see how it works. If you add your own images named image1.png and image2.png to the playground's resources, they will be used (otherwise, this example creates solid blue and solid green images):
import UIKit
import PlaygroundSupport
// UIImage extension to create a new, solid-color image
public extension UIImage {
public convenience init?(color: UIColor, size: CGSize = CGSize(width: 1, height: 1)) {
let rect = CGRect(origin: .zero, size: size)
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
color.setFill()
UIRectFill(rect)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
guard let cgImage = image?.cgImage else { return nil }
self.init(cgImage: cgImage)
}
}
class TestViewController : UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// create a UIScrollView
let scrollView = UIScrollView()
// we will set the auto-layout constraints
scrollView.translatesAutoresizingMaskIntoConstraints = false
// set background color so we can see the scrollView when the images are scrolled
scrollView.backgroundColor = .orange
// add the scrollView to the view
view.addSubview(scrollView)
// pin scrollView 20-pts from top/bottom/leading/trailing
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20.0).isActive = true
scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 20.0).isActive = true
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20.0).isActive = true
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -20.0).isActive = true
// create an array of empty images in case this is run without
// valid images in the resources
var imgArray = [UIImage(color: .blue), UIImage(color: .green)]
// if these images exist, load them and replace the blank images in imgArray
if let img1: UIImage = UIImage(named: "image1"),
let img2: UIImage = UIImage(named: "image2") {
imgArray = [img1, img2]
}
// create a UIStackView
let stackView = UIStackView()
// we can use the default stackView properties
// but can change axis, alignment, distribution, spacing, etc if desired
// we will set the auto-layout constraints
stackView.translatesAutoresizingMaskIntoConstraints = false
// add the stackView to the scrollView
scrollView.addSubview(stackView)
// with auto-layout, scroll views use the content's constraints to
// determine the contentSize,
// so pin the stackView to top/bottom/leading/trailing of the scrollView
stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 0.0).isActive = true
stackView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 0.0).isActive = true
stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: 0.0).isActive = true
stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 0.0).isActive = true
// loop through the images
for img in imgArray {
// create a new UIImageView
let imgView = UIImageView(image: img)
// we will set the auto-layout constraints, and allow the stackView
// to handle the placement
imgView.translatesAutoresizingMaskIntoConstraints = false
// set image scaling as desired
imgView.contentMode = .scaleToFill
// add the image view to the stackView
stackView.addArrangedSubview(imgView)
// set imgView's width and height to the scrollView's width and height
imgView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, multiplier: 1.0).isActive = true
imgView.heightAnchor.constraint(equalTo: scrollView.heightAnchor, multiplier: 1.0).isActive = true
}
}
}
let vc = TestViewController()
vc.view.backgroundColor = .red
PlaygroundPage.current.liveView = vc
I modified my code and tried as follows and its working now. with page contrlller
let imgArray = [UIImage]()
let img: UIImage = self.dataDict.object(forKey: kCardImgFront) as! UIImage
let img2:UIImage = self.dataDict.object(forKey: kCardImgBack) as! UIImage
imgArray = [img, img2]
for i in 0..<imgArray.count {
let imageView = UIImageView()
imageView.image = imgArray[i]
let xPosition = self.view.frame.width * CGFloat(i)
imageView.frame = CGRect(x: xPosition, y: 0, width:
self.scrollView.frame.width + 50, height: self.scrollView.frame.height)
scrollView.contentSize.width = scrollView.frame.width * CGFloat(i + 1)
scrollView.addSubview(imageView)
}
self.scrollView.delegate = self
func scrollViewDidScroll(_ scrollView: UIScrollView){
pageController.currentPage = Int(self.scrollView.contentOffset.x /
CGFloat(4))
}
I think you need to set a proper frame for the cardImgView. It would be something like
cardImgView.frame = CGRect(x: scrollView.frame.width * CGFloat(i), y: 0, width: scrollView.frame.width, height: scrollView.frame.height)
Finally, after the for loop, you need to set scroll view's content size:
scrollView.contentSize.width = scrollView.frame.width * imgArray.count
Hope this helps.
I have written scrolling images horizontally in swift. Please check with this:
import UIKit
class ViewController: UIViewController,UIScrollViewDelegate {
#IBOutlet weak var Bannerview: UIView!
var spinner = UIActivityIndicatorView(activityIndicatorStyle: .whiteLarge)
var loadingView: UIView = UIView()
var loadinglabel: UILabel = UILabel()
var nextPage :Int!
var titlelab :UILabel!
var bannerimg :UIImageView!
var scroll :UIScrollView!
var viewPanel :UIView!
var pgCtr:UIPageControl!
var bannerArr:[String]!
var imgUrlstr :NSString!
var screenSize: CGRect!
var screenWidth: CGFloat!
var screenHeight: CGFloat!
func uicolorFromHex(rgbValue:UInt32)->UIColor
{
let red = CGFloat((rgbValue & 0xFF0000) >> 16)/256.0
let green = CGFloat((rgbValue & 0xFF00) >> 8)/256.0
let blue = CGFloat(rgbValue & 0xFF)/256.0
return UIColor(red:red, green:green, blue:blue, alpha:1.0)
}
override func viewWillAppear(_ animated: Bool)
{
screenSize = UIScreen.main.bounds
screenWidth = screenSize.width
screenHeight = screenSize.height
bannerArr = ["image1.jpeg","image2.jpeg","image3.jpeg","images4.jpeg","images5.jpeg"]
self.bannerview()
self.navigationController?.setNavigationBarHidden(false, animated: true)
self.navigationController?.navigationBar.isTranslucent = false
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
}
I stripped down my code so that its easy to understand.
Say you have a controller and you want to add a simple scroller using pure auto layout.
You can invoke my function tool (provided below) as follows:
// Create scroll view
let strip = addStripCategoryTo(view)
// Attach it to the view, vertically and horizontally
strip.topAnchor.constraintEqualToAnchor(view.topAnchor).active = true
strip.leftAnchor.constraintEqualToAnchor(view.leftAnchor).active = true
// The function
func addStripCategoryTo(parent: UIView) -> UIView {
let h:CGFloat = 128
let w = 2*h/3
let n = 5
let width = w * CGFloat(n)
let height = h
// Scroll view
let scrollview = UIScrollView()
parent.addSubview(scrollview)
scrollview.scrollEnabled = true
scrollview.translatesAutoresizingMaskIntoConstraints = false
scrollview.widthAnchor .constraintEqualToAnchor(parent.widthAnchor).active = true
scrollview.heightAnchor.constraintEqualToConstant(h).active = true
scrollview.layer.borderWidth = 2
scrollview.layer.borderColor = UIColor.greenColor().CGColor
scrollview.backgroundColor = UIColor.brownColor()
// Scroll view content
let contentView = UIView() //frame:CGRect(origin: CGPointZero, size:CGSize(width: width, height: height)))
scrollview.addSubview(contentView)
contentView.backgroundColor = UIColor.redColor()
contentView.layer.borderWidth = 10
contentView.layer.borderColor = UIColor.blueColor().CGColor
contentView.translatesAutoresizingMaskIntoConstraints = false
contentView.centerYAnchor.constraintEqualToAnchor(scrollview.centerYAnchor).active = true
contentView.widthAnchor .constraintEqualToConstant(width).active = true
contentView.heightAnchor.constraintEqualToConstant(height).active = true
return scrollview
}
Unfortunately, I cannot scroll it horizontally, see screenshot as follows:
What am I missing?
Your constraints should be like this,
Scrollview - top,bottom,leading,trailing
ContentView - top,bottom,leading,trailing, fixed width, vertically center in container (center y)
and your content view's width should be large than screen width then also you can scroll horizontally
Hope this will help :)
Going to give this a try and answer this, see if it helps.
EDIT
Answering question better, old answer deleted:
Adding these two lines should make to scroll horizontally:
contentView.leadingAnchor.constraintEqualToAnchor(scrollview.leadingAnchor).active = true contentView.trailingAnchor.constraintEqualToAnchor(scrollview.trailingAnchor).active = true
The problem is that without these two constraint, the scrollView doesn't know its contentSize, therefor not scrolling, even though you gave the contentView a specific width. You need to let know the scrollView where does that contentView start and end, and these two constraints will help the scrollView know how big is the contentsView width. If not the scrollView's contentSize is going to stay the same as the device's width, and the contentView is just going to show offscreen since it's width is larger than the device's screen.
Hope this helped better.
Full code of the function
func addStripCategoryTo(parent: UIView)-> UIView {
let h:CGFloat = 128
let w = 2*h/3
let n = 5
let width = w * CGFloat(n)
let height = h
// Scroll view
let scrollview = UIScrollView()
parent.addSubview(scrollview)
scrollview.scrollEnabled = true
scrollview.translatesAutoresizingMaskIntoConstraints = false
scrollview.leadingAnchor.constraintEqualToAnchor(parent.leadingAnchor).active = true
scrollview.trailingAnchor.constraintEqualToAnchor(parent.trailingAnchor).active = true
scrollview.widthAnchor .constraintEqualToAnchor(parent.widthAnchor).active = true
scrollview.heightAnchor.constraintEqualToConstant(h).active = true
scrollview.layer.borderWidth = 2
scrollview.layer.borderColor = UIColor.greenColor().CGColor
scrollview.backgroundColor = UIColor.brownColor()
// Scroll view content
let contentView = UIView() //frame:CGRect(origin: CGPointZero, size:CGSize(width: width, height: height)))
scrollview.addSubview(contentView)
contentView.backgroundColor = UIColor.redColor()
contentView.layer.borderWidth = 10
contentView.layer.borderColor = UIColor.blueColor().CGColor
contentView.translatesAutoresizingMaskIntoConstraints = false
contentView.centerYAnchor.constraintEqualToAnchor(scrollview.centerYAnchor).active = true
contentView.leadingAnchor.constraintEqualToAnchor(scrollview.leadingAnchor).active = true
contentView.trailingAnchor.constraintEqualToAnchor(scrollview.trailingAnchor).active = true
contentView.widthAnchor.constraintEqualToConstant(width).active = true
contentView.heightAnchor.constraintEqualToConstant(height).active = true
return scrollview
}
Here is the solution of my issue. I provided a tool function that solve the issue as follow:
func setScrollViewConstraints(scrollView: UIScrollView, contentView:UIView, multiplier m:(width:CGFloat, height:CGFloat)=(1, 1)) {
guard let scrollViewParent = scrollView.superview else {
assert(false, "setScrollViewConstraints() requires the scrollview to have a superview")
return
}
guard let contentViewParent = contentView.superview else {
assert(false, "setScrollViewConstraints() requires the contentView to have a superview")
return
}
guard contentViewParent == scrollView else {
assert(false, "setScrollViewConstraints() requires the contentView to have a superview being the scrollView")
return
}
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.widthAnchor .constraintEqualToAnchor(scrollViewParent.widthAnchor, multiplier: m.width ).active = true
scrollView.heightAnchor .constraintEqualToAnchor(scrollViewParent.heightAnchor, multiplier: m.height).active = true
contentView.translatesAutoresizingMaskIntoConstraints = false
contentView.leftAnchor .constraintEqualToAnchor(contentViewParent.leftAnchor ).active = true
contentView.rightAnchor .constraintEqualToAnchor(contentViewParent.rightAnchor ).active = true
contentView.topAnchor .constraintEqualToAnchor(contentViewParent.topAnchor ).active = true
contentView.bottomAnchor.constraintEqualToAnchor(contentViewParent.bottomAnchor ).active = true
/*
// Pre-iOS 9 version
let views = [
"scrollView":scrollview,
"contentView":contentView
]
parent.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[scrollView]|", options:[], metrics: [:], views:views))
parent.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[scrollView]|", options:[], metrics: [:], views:views))
scrollview.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[contentView]|", options:[], metrics: [:], views:views))
scrollview.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[contentView]|", options:[], metrics: [:], views:views))
*/
}
Use it like this for having the scroll view 100% width the super view and 1/4th of its height:
func addStripCategoryTo(parent: UIView) -> UIView {
// Scroll view
let scrollview = UIScrollView()
parent.addSubview(scrollview)
scrollview.backgroundColor = UIColor.brownColor()
scrollview.layer.borderWidth = 2
scrollview.layer.borderColor = UIColor.greenColor().CGColor
// Scroll view content
let contentView = UIImageView()
contentView.image = UIImage(named: "cover-0.jpeg")
scrollview.addSubview(contentView)
contentView.backgroundColor = UIColor.redColor()
contentView.layer.borderWidth = 10
contentView.layer.borderColor = UIColor.blueColor().CGColor
// Apply constraints
setScrollViewConstraints(scrollview, contentView:contentView, multiplier: (1, 0.25))
return scrollview
}