Zoomable fullscreen image in portrait and landscape mode - swift

I am a beginner at programming. I have an app where you can tap into any of the images that are on display to show that image in full-screen mode and can pinch to zoom. The issue I am having is that if you rotate the phone then the image is only half visible.
I'm using a scroll view to achieve the zoom functionality as that seems to be the consensus of the best way to do it.
It works perfectly in portrait mode, or if I enter the fullscreen image while the app is already in landscape orientation, but If I go into the landscape while in the fullscreen image that's where it goes wrong. Here is the code:
class PictureDetailViewController: UIViewController, UIScrollViewDelegate {
#IBOutlet weak var scrollView: UIScrollView!
var routeData = IndividualRoute(numberOfRoute: UserDefaults.standard.integer(forKey: "currentRoute"))
var detailPicture = UserDefaults.standard.bool(forKey: "segueFromDetailvc")
var detailImage = UIImageView()
override func viewDidLoad() {
super.viewDidLoad()
routeData.routesValues()
scrollView.delegate = self
selectImage()
scrollView.frame = UIScreen.main.bounds
scrollView.addSubview(detailImage)
scrollViewContents()
setupConstraints()
let scrollViewFrame = scrollView.frame
let scaleWidth = scrollViewFrame.size.width / scrollView.contentSize.width
let scaleHieght = scrollViewFrame.size.height / scrollView.contentSize.height
let minScale = min(scaleHieght, scaleWidth)
scrollView.minimumZoomScale = minScale
scrollView.maximumZoomScale = 1
scrollView.zoomScale = minScale
let tap = UITapGestureRecognizer(target: self, action: #selector(dismissFullscreen))
tap.numberOfTapsRequired = 2
view.addGestureRecognizer(tap)
}
//****************************************
//Image Setup
func setupConstraints() {
scrollView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
scrollView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
scrollView.centerYAnchor.constraint(equalTo: view.centerYAnchor)])
scrollView.contentSize = (detailImage.image?.size)!
}
func selectImage() {
if !detailPicture {
let foo = "\(routeData.achievements[UserDefaults.standard.integer(forKey: "currentAchievement")])"
detailImage.image = UIImage(named: "\(foo.folding(options: .diacriticInsensitive, locale: nil)) 0")
} else {
let foo = "\(routeData.achievements[0])"
detailImage.image = UIImage(named: "\(foo.folding(options: .diacriticInsensitive, locale: nil)) 0")
print("\(foo.folding(options: .diacriticInsensitive, locale: nil)) 0")
}
guard let width = detailImage.image?.size.width else {
return
}
guard let height = detailImage.image?.size.height else {
return
}
let frame: CGRect = CGRect(x: 0, y: 0, width: width, height: height)
detailImage.frame = frame
detailImage.isUserInteractionEnabled = true
}
//**************************************
//Scrollview setup
#objc func dismissFullscreen(){
scrollView.setZoomScale(1, animated: true)
}
func scrollViewContents() {
let boundSize = UIScreen.main.bounds.size
var contentFrame = detailImage.frame
if contentFrame.size.width < boundSize.width {
contentFrame.origin.x = (boundSize.width - contentFrame.size.width) / 2
} else {
contentFrame.origin.x = 0
}
if contentFrame.size.height < boundSize.height {
contentFrame.origin.y = (boundSize.height - contentFrame.size.height) / 2
} else {
contentFrame.origin.y = 0
}
detailImage.frame = contentFrame
}
func scrollViewDidZoom(_ scrollView: UIScrollView) {
scrollViewContents()
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return detailImage
}
Sorry for posting so much code but its pretty much all relevant (I think).
Here are screenshots of the problem:

Okay, I've managed to fix it! Had a little jump around with delight.
I set up a new function called setupScale() that is called in viewdidload when the view is presented. I also added the viewwillLayoutSubview() override and called the setupScale() function inside it.
If looks like this:
private func setupScale() {
scrollView.frame = UIScreen.main.bounds
scrollView.contentSize = (detailImage.image?.size)!
scrollViewContents()
let scrollViewFrame = scrollView.frame
let scaleWidth = scrollViewFrame.size.width / scrollView.contentSize.width
let scaleHieght = scrollViewFrame.size.height / scrollView.contentSize.height
let minScale = min(scaleHieght, scaleWidth)
scrollView.minimumZoomScale = minScale
scrollView.maximumZoomScale = 1
scrollView.zoomScale = minScale
}
override func viewWillLayoutSubviews() {
setupScale()
}
Results look perfect on my iPad and iPhone 7 in landscape and portrait.

Related

Sizing UIButton depending on length of titleLabel

So I have a UIButton and I'm setting the title in it to a string that is dynamic in length. I want the width of the titleLabel to be half of the screen width. I've tried using .sizeToFit() but this causes the button to use the CGSize before the constraint was applied to the titleLabel. I tried using .sizeThatFits(button.titleLabel?.intrinsicContentSize) but this also didn't work. I think the important functions below are the init() & presentCallout(), but I'm showing the entire class just for a more complete understanding. The class I'm playing with looks like:
class CustomCalloutView: UIView, MGLCalloutView {
var representedObject: MGLAnnotation
// Allow the callout to remain open during panning.
let dismissesAutomatically: Bool = false
let isAnchoredToAnnotation: Bool = true
// https://github.com/mapbox/mapbox-gl-native/issues/9228
override var center: CGPoint {
set {
var newCenter = newValue
newCenter.y -= bounds.midY
super.center = newCenter
}
get {
return super.center
}
}
lazy var leftAccessoryView = UIView() /* unused */
lazy var rightAccessoryView = UIView() /* unused */
weak var delegate: MGLCalloutViewDelegate?
let tipHeight: CGFloat = 10.0
let tipWidth: CGFloat = 20.0
let mainBody: UIButton
required init(representedObject: MGLAnnotation) {
self.representedObject = representedObject
self.mainBody = UIButton(type: .system)
super.init(frame: .zero)
backgroundColor = .clear
mainBody.backgroundColor = .white
mainBody.tintColor = .black
mainBody.contentEdgeInsets = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0)
mainBody.layer.cornerRadius = 4.0
addSubview(mainBody)
// I thought this would work, but it doesn't.
// mainBody.translatesAutoresizingMaskIntoConstraints = false
// mainBody.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
// mainBody.leftAnchor.constraint(equalTo: self.rightAnchor).isActive = true
// mainBody.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true
// mainBody.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
}
required init?(coder decoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - MGLCalloutView API
func presentCallout(from rect: CGRect, in view: UIView, constrainedTo constrainedRect: CGRect, animated: Bool) {
delegate?.calloutViewWillAppear?(self)
view.addSubview(self)
// Prepare title label.
mainBody.setTitle(representedObject.title!, for: .normal)
mainBody.titleLabel?.lineBreakMode = .byWordWrapping
mainBody.titleLabel?.numberOfLines = 0
mainBody.sizeToFit()
if isCalloutTappable() {
// Handle taps and eventually try to send them to the delegate (usually the map view).
mainBody.addTarget(self, action: #selector(CustomCalloutView.calloutTapped), for: .touchUpInside)
} else {
// Disable tapping and highlighting.
mainBody.isUserInteractionEnabled = false
}
// Prepare our frame, adding extra space at the bottom for the tip.
let frameWidth = mainBody.bounds.size.width
let frameHeight = mainBody.bounds.size.height + tipHeight
let frameOriginX = rect.origin.x + (rect.size.width/2.0) - (frameWidth/2.0)
let frameOriginY = rect.origin.y - frameHeight
frame = CGRect(x: frameOriginX, y: frameOriginY, width: frameWidth, height: frameHeight)
if animated {
alpha = 0
UIView.animate(withDuration: 0.2) { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.alpha = 1
strongSelf.delegate?.calloutViewDidAppear?(strongSelf)
}
} else {
delegate?.calloutViewDidAppear?(self)
}
}
func dismissCallout(animated: Bool) {
if (superview != nil) {
if animated {
UIView.animate(withDuration: 0.2, animations: { [weak self] in
self?.alpha = 0
}, completion: { [weak self] _ in
self?.removeFromSuperview()
})
} else {
removeFromSuperview()
}
}
}
// MARK: - Callout interaction handlers
func isCalloutTappable() -> Bool {
if let delegate = delegate {
if delegate.responds(to: #selector(MGLCalloutViewDelegate.calloutViewShouldHighlight)) {
return delegate.calloutViewShouldHighlight!(self)
}
}
return false
}
#objc func calloutTapped() {
if isCalloutTappable() && delegate!.responds(to: #selector(MGLCalloutViewDelegate.calloutViewTapped)) {
delegate!.calloutViewTapped!(self)
}
}
// MARK: - Custom view styling
override func draw(_ rect: CGRect) {
// Draw the pointed tip at the bottom.
let fillColor: UIColor = .white
let tipLeft = rect.origin.x + (rect.size.width / 2.0) - (tipWidth / 2.0)
let tipBottom = CGPoint(x: rect.origin.x + (rect.size.width / 2.0), y: rect.origin.y + rect.size.height)
let heightWithoutTip = rect.size.height - tipHeight - 1
let currentContext = UIGraphicsGetCurrentContext()!
let tipPath = CGMutablePath()
tipPath.move(to: CGPoint(x: tipLeft, y: heightWithoutTip))
tipPath.addLine(to: CGPoint(x: tipBottom.x, y: tipBottom.y))
tipPath.addLine(to: CGPoint(x: tipLeft + tipWidth, y: heightWithoutTip))
tipPath.closeSubpath()
fillColor.setFill()
currentContext.addPath(tipPath)
currentContext.fillPath()
}
}
This is what it looks like for a short title and a long title. When the title gets too long, I want the text to wrap and the bubble to get a taller height. As you can see in the image set below, the first 'Short Name' works fine as a map annotation bubble. When the name gets super long though, it just widens the bubble to the point it goes off the screen.
https://imgur.com/a/I5z0zUd
Any help on how to fix is much appreciated. Thanks!
To enable word-wrapping to multiple lines in a UIButton, you need to create your own button subclass.
For example:
class MultilineTitleButton: UIButton {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
func commonInit() -> Void {
self.titleLabel?.numberOfLines = 0
self.titleLabel?.textAlignment = .center
self.setContentHuggingPriority(UILayoutPriority.defaultLow + 1, for: .vertical)
self.setContentHuggingPriority(UILayoutPriority.defaultLow + 1, for: .horizontal)
}
override var intrinsicContentSize: CGSize {
let size = self.titleLabel!.intrinsicContentSize
return CGSize(width: size.width + contentEdgeInsets.left + contentEdgeInsets.right, height: size.height + contentEdgeInsets.top + contentEdgeInsets.bottom)
}
override func layoutSubviews() {
super.layoutSubviews()
titleLabel?.preferredMaxLayoutWidth = self.titleLabel!.frame.size.width
}
}
That button will wrap the title onto multiple lines, cooperating with auto-layout / constraints.
I don't have any projects with MapBox, but here is an example using a modified version of your CustomCalloutView. I commented out any MapBox specific code. You may be able to un-comment those lines and use this as-is:
class CustomCalloutView: UIView { //}, MGLCalloutView {
//var representedObject: MGLAnnotation
var repTitle: String = ""
// Allow the callout to remain open during panning.
let dismissesAutomatically: Bool = false
let isAnchoredToAnnotation: Bool = true
// https://github.com/mapbox/mapbox-gl-native/issues/9228
// NOTE: this causes a vertical shift when NOT using MapBox
// override var center: CGPoint {
// set {
// var newCenter = newValue
// newCenter.y -= bounds.midY
// super.center = newCenter
// }
// get {
// return super.center
// }
// }
lazy var leftAccessoryView = UIView() /* unused */
lazy var rightAccessoryView = UIView() /* unused */
//weak var delegate: MGLCalloutViewDelegate?
let tipHeight: CGFloat = 10.0
let tipWidth: CGFloat = 20.0
let mainBody: UIButton
var anchorView: UIView!
override func willMove(toSuperview newSuperview: UIView?) {
if newSuperview == nil {
anchorView.removeFromSuperview()
}
}
//required init(representedObject: MGLAnnotation) {
required init(title: String) {
self.repTitle = title
self.mainBody = MultilineTitleButton()
super.init(frame: .zero)
backgroundColor = .clear
mainBody.backgroundColor = .white
mainBody.setTitleColor(.black, for: [])
mainBody.tintColor = .black
mainBody.contentEdgeInsets = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0)
mainBody.layer.cornerRadius = 4.0
addSubview(mainBody)
mainBody.translatesAutoresizingMaskIntoConstraints = false
let padding: CGFloat = 8.0
NSLayoutConstraint.activate([
mainBody.topAnchor.constraint(equalTo: self.topAnchor, constant: padding),
mainBody.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: padding),
mainBody.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -padding),
mainBody.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -padding),
])
}
required init?(coder decoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - MGLCalloutView API
func presentCallout(from rect: CGRect, in view: UIView, constrainedTo constrainedRect: CGRect, animated: Bool) {
//delegate?.calloutViewWillAppear?(self)
// since we'll be using auto-layout for the mutli-line button
// we'll add an "anchor view" to the superview
// it will be removed when self is removed
anchorView = UIView(frame: rect)
anchorView.isUserInteractionEnabled = false
anchorView.backgroundColor = .clear
view.addSubview(anchorView)
view.addSubview(self)
// Prepare title label.
//mainBody.setTitle(representedObject.title!, for: .normal)
mainBody.setTitle(self.repTitle, for: .normal)
// if isCalloutTappable() {
// // Handle taps and eventually try to send them to the delegate (usually the map view).
// mainBody.addTarget(self, action: #selector(CustomCalloutView.calloutTapped), for: .touchUpInside)
// } else {
// // Disable tapping and highlighting.
// mainBody.isUserInteractionEnabled = false
// }
self.translatesAutoresizingMaskIntoConstraints = false
anchorView.autoresizingMask = [.flexibleTopMargin, .flexibleLeftMargin, .flexibleRightMargin, .flexibleBottomMargin]
NSLayoutConstraint.activate([
self.centerXAnchor.constraint(equalTo: anchorView.centerXAnchor),
self.bottomAnchor.constraint(equalTo: anchorView.topAnchor),
self.widthAnchor.constraint(lessThanOrEqualToConstant: constrainedRect.width),
])
if animated {
alpha = 0
UIView.animate(withDuration: 0.2) { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.alpha = 1
//strongSelf.delegate?.calloutViewDidAppear?(strongSelf)
}
} else {
//delegate?.calloutViewDidAppear?(self)
}
}
func dismissCallout(animated: Bool) {
if (superview != nil) {
if animated {
UIView.animate(withDuration: 0.2, animations: { [weak self] in
self?.alpha = 0
}, completion: { [weak self] _ in
self?.removeFromSuperview()
})
} else {
removeFromSuperview()
}
}
}
// MARK: - Callout interaction handlers
// func isCalloutTappable() -> Bool {
// if let delegate = delegate {
// if delegate.responds(to: #selector(MGLCalloutViewDelegate.calloutViewShouldHighlight)) {
// return delegate.calloutViewShouldHighlight!(self)
// }
// }
// return false
// }
//
// #objc func calloutTapped() {
// if isCalloutTappable() && delegate!.responds(to: #selector(MGLCalloutViewDelegate.calloutViewTapped)) {
// delegate!.calloutViewTapped!(self)
// }
// }
// MARK: - Custom view styling
override func draw(_ rect: CGRect) {
print(#function)
// Draw the pointed tip at the bottom.
let fillColor: UIColor = .red
let tipLeft = rect.origin.x + (rect.size.width / 2.0) - (tipWidth / 2.0)
let tipBottom = CGPoint(x: rect.origin.x + (rect.size.width / 2.0), y: rect.origin.y + rect.size.height)
let heightWithoutTip = rect.size.height - tipHeight - 1
let currentContext = UIGraphicsGetCurrentContext()!
let tipPath = CGMutablePath()
tipPath.move(to: CGPoint(x: tipLeft, y: heightWithoutTip))
tipPath.addLine(to: CGPoint(x: tipBottom.x, y: tipBottom.y))
tipPath.addLine(to: CGPoint(x: tipLeft + tipWidth, y: heightWithoutTip))
tipPath.closeSubpath()
fillColor.setFill()
currentContext.addPath(tipPath)
currentContext.fillPath()
}
}
Here is a sample view controller showing that "Callout View" with various length titles, restricted to 70% of the width of the view:
class CalloutTestVC: UIViewController {
let sampleTitles: [String] = [
"Short Title",
"Slightly Longer Title",
"A ridiculously long title that will need to wrap!",
]
var idx: Int = -1
let tapView = UIView()
var ccv: CustomCalloutView!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(red: 0.8939146399, green: 0.8417750597, blue: 0.7458069921, alpha: 1)
tapView.backgroundColor = .systemBlue
tapView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tapView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
tapView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
tapView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
tapView.widthAnchor.constraint(equalToConstant: 60),
tapView.heightAnchor.constraint(equalTo: tapView.widthAnchor),
])
// tap the Blue View to cycle through Sample Titles for the Callout View
// using the Blue view as the "anchor rect"
let t = UITapGestureRecognizer(target: self, action: #selector(gotTap))
tapView.addGestureRecognizer(t)
}
#objc func gotTap() -> Void {
if ccv != nil {
ccv.removeFromSuperview()
}
// increment sampleTitles array index
// to cycle through the strings
idx += 1
let validIdx = idx % sampleTitles.count
let str = sampleTitles[validIdx]
// create a new Callout view
ccv = CustomCalloutView(title: str)
// to restrict the "callout view" width to less-than 1/2 the screen width
// use view.width * 0.5 for the constrainedTo width
// may look better restricting it to 70%
ccv.presentCallout(from: tapView.frame, in: self.view, constrainedTo: CGRect(x: 0, y: 0, width: view.frame.size.width * 0.7, height: 100), animated: false)
}
}
It looks like this:
The UIButton class owns the titleLabel and is going to position and set the constraints on that label itself. More likely than not you are going to have to create a subclass of UIButton and override its "updateConstraints" method to position the titleLabel where you want it to go.
Your code should probably not be basing the size of the button off the size of the screen. It might set the size of off some other view in your hierarchy that happens to be the size of the screen but grabbing the screen bounds in the middle of setting a view's size is unusual.

align object with changeable constraints to center x point on the view

I want my swift code below to align the box to the center of the x axis. You can see from the gif below of what my code is doing. When the purple button is pressed I would like the box to be align. I am not sure how to d this because some of the constraints are declared as var I dont know where to go next.
import UIKit
class ViewController: UIViewController {
var slizer = UISlider()
var viewDrag = UIImageView()
var b2 = UIButton()
var panGesture = UIPanGestureRecognizer()
// Width, Leading and CenterY constraints for viewDrag
var widthConstraints: NSLayoutConstraint!
var viewDragLeadingConstraint: NSLayoutConstraint!
var viewDragCenterYConstraint: NSLayoutConstraint!
var tim: CGFloat = 50.0
var slidermultiplier: CGFloat = 0.6
override func viewDidLoad() {
super.viewDidLoad()
[viewDrag,slizer,b2].forEach{
$0.translatesAutoresizingMaskIntoConstraints = false
view.addSubview($0)
}
b2.backgroundColor = .purple
NSLayoutConstraint.activate([
b2.bottomAnchor.constraint(equalTo: slizer.topAnchor),
b2.leadingAnchor.constraint(equalTo: view.leadingAnchor),
b2.heightAnchor.constraint(equalTo: view.heightAnchor,multiplier: 0.05),
b2.widthAnchor.constraint(equalTo: view.widthAnchor,multiplier: 1),
slizer.bottomAnchor.constraint(equalTo: view.bottomAnchor),
slizer.leadingAnchor.constraint(equalTo: view.leadingAnchor),
slizer.heightAnchor.constraint(equalTo: view.heightAnchor,multiplier: 0.2),
slizer.widthAnchor.constraint(equalTo: view.widthAnchor,multiplier: 1),
])
slizer.addTarget(self, action: #selector(increase), for: .valueChanged)
viewDrag.backgroundColor = .orange
// no point setting a frame, since
// viewDrag has .translatesAutoresizingMaskIntoConstraints = false
//viewDrag.frame = CGRect(x: view.center.x-view.frame.width * 0.05, y: view.center.y-view.frame.height * 0.05, width: view.frame.width * 0.1, height: view.frame.height * 0.1)
// start with viewDrag
// width = "slidermultiplier" percent of view width
widthConstraints = viewDrag.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: slidermultiplier)
// Leading = "tim" pts from view leading
viewDragLeadingConstraint = viewDrag.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: tim)
// centered vertically
viewDragCenterYConstraint = viewDrag.centerYAnchor.constraint(equalTo: view.centerYAnchor)
NSLayoutConstraint.activate([
// viewDrag height will never change, so we can set it here
viewDrag.heightAnchor.constraint(equalTo: view.heightAnchor,multiplier: 0.3),
// activate the 3 "modifiable" constraints
widthConstraints,
viewDragLeadingConstraint,
viewDragCenterYConstraint,
])
panGesture = UIPanGestureRecognizer(target: self, action: #selector(draggedView(_:)))
viewDrag.isUserInteractionEnabled = true
viewDrag.addGestureRecognizer(panGesture)
// start the slider at the same percentage we've used
// for viewDrag's initial width
slizer.value = Float(slidermultiplier)
b2.addTarget(self, action: #selector(press), for: .touchDown)
}
#objc func press(){
}
#objc func draggedView(_ sender: UIPanGestureRecognizer) {
// old swift syntax
//self.view.bringSubview(toFront: viewDrag)
self.view.bringSubview(toFront: viewDrag)
let translation = sender.translation(in: self.view)
viewDragLeadingConstraint.constant += translation.x
viewDragCenterYConstraint.constant += translation.y
// don't do this
//viewDrag.center = CGPoint(x: viewDrag.center.x + translation.x , y: viewDrag.center.y + translation.y)
sender.setTranslation(CGPoint.zero, in: self.view)
}
#objc func increase() {
// get the new value of the slider
slidermultiplier = CGFloat(slizer.value)
// deactivate widthConstraints
widthConstraints.isActive = false
// create new widthConstraints with slider value as a multiplier
widthConstraints = viewDrag.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: slidermultiplier)
// activate the new widthConstraints
widthConstraints.isActive = true
}
}
I recommend to work directly with frame not constraints.
Constraints are suitable when to deal with mutable screen sizes and screen rotation.
Step 1: Put your movable objects into a container view
Step 2: Handle objects by coordinate inside container view
Step 3: Add container to your view controller's view and constraint layouting it with other elements
class ViewController: UIViewController {
#IBOutlet weak var container: Container!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
container.addPanGestures()
}
#IBAction func didTapLeftButton(_ sender: Any) {
container.setLeftAlign()
}
#IBAction func didTapCenterButton(_ sender: Any) {
container.setCenterXAlign()
}
#IBAction func didTapRightButton(_ sender: Any) {
container.setRightAlign()
}
#IBAction func slide(_ sender: UISlider) {
let scale = CGFloat(max(sender.value, 0.1))
container.setWidthScale(scale)
}
}
class Container: UIView {
func setLeftAlign() {
for view in self.subviews {
view.frame.origin = CGPoint(x: 0, y: view.frame.origin.y)
}
}
func setRightAlign() {
for view in self.subviews {
view.frame.origin = CGPoint(x: self.frame.width - view.frame.width, y:view.frame.origin.y)
}
}
func setCenterXAlign() {
for view in self.subviews {
var center = view.center
center.x = self.frame.width / 2
view.center = center
}
}
func setWidthScale(_ ratio: CGFloat) {
for view in self.subviews {
var frame = view.frame
let center = view.center
frame.size.width = self.frame.width * ratio
view.frame = frame
view.center = center
}
}
func addPanGestures() {
for v in self.subviews {
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(didPan(_:)))
v.addGestureRecognizer(panGesture)
}
}
private var initCenter: CGPoint!
#objc func didPan(_ sender: UIPanGestureRecognizer) {
let view = sender.view!
let translation = sender.translation(in: view)
switch sender.state {
case .began:
initCenter = view.center
case .changed:
view.center = CGPoint(x: initCenter.x + translation.x, y: initCenter.y + translation.y)
case .cancelled:
view.center = initCenter
default:
return
}
}
}

UIScrollView with Embedded UIImageView; how to get the image to fill the screen

UIKit/Programmatic UI
I have an UIScrollView with an UIImageView inside. The image is set by user selection and can have all kinds of sizes. What I want is that the image initially fills the screen (view) and then can be zoomed and scrolled all the way to the edges of the image.
If I add the ImageView directly to the view (no scrollView), I get it to fill the screen with the following code:
mapImageView.image = ProjectImages.projectDefaultImage
mapImageView.translatesAutoresizingMaskIntoConstraints = false
mapImageView.contentMode = .scaleAspectFill
view.addSubview(mapImageView)
Now the same with the scrollView and the embedded imageView:
view.insertSubview(mapImageScrollView, at: 0)
mapImageScrollView.delegate = self
mapImageScrollView.translatesAutoresizingMaskIntoConstraints = false
mapImageScrollView.contentMode = .scaleAspectFill
mapImageScrollView.maximumZoomScale = 4.0
mapImageScrollView.pinToEdges(of: view, safeArea: true)
mapImageView.image = ProjectImages.projectDefaultImage
mapImageView.translatesAutoresizingMaskIntoConstraints = false
mapImageView.contentMode = .scaleAspectFill
mapImageScrollView.addSubview(mapImageView)
And now, if the image's height is smaller than the view's height, the image does not fill the screen and I'm left with a blank view area below the image. I can zoom and scroll ok, and then the image does fill the view.
Adding contsraints will fill the view as I want, but interferes with the zooming and scrolling and prevents me getting to the edges of the image when zoomed in.
How to set this up correctly ?
You might find this useful...
It allows you to zoom an image in a scrollView, starting with it centered and maintaining aspect ratio.
Here's a complete implementation. It has two important variables at the top:
// can be .scaleAspectFill or .scaleAspectFit
var fitMode: UIView.ContentMode = .scaleAspectFill
// if fitMode is .scaleAspectFit, allowFullImage is ignored
// if fitMode is .scaleAspectFill, image will start zoomed to .scaleAspectFill
// if allowFullImage is false, image will zoom back to .scaleAspectFill if "pinched in"
// if allowFullImage is true, image can be "pinched in" to see the full image
var allowFullImage: Bool = true
Everything is done via code - no #IBOutlet or other connections - so just create add a new view controller and assign its custom class to ZoomAspectViewController (and edit the name of the image you want to use):
class ZoomAspectViewController: UIViewController, UIScrollViewDelegate {
var scrollView: UIScrollView!
var imageView: UIImageView!
var imageViewBottomConstraint: NSLayoutConstraint!
var imageViewLeadingConstraint: NSLayoutConstraint!
var imageViewTopConstraint: NSLayoutConstraint!
var imageViewTrailingConstraint: NSLayoutConstraint!
// can be .scaleAspectFill or .scaleAspectFit
var fitMode: UIView.ContentMode = .scaleAspectFit
// if fitMode is .scaleAspectFit, allowFullImage is ignored
// if fitMode is .scaleAspectFill, image will start zoomed to .scaleAspectFill
// if allowFullImage is false, image will zoom back to .scaleAspectFill if "pinched in"
// if allowFullImage is true, image can be "pinched in" to see the full image
var allowFullImage: Bool = true
override func viewDidLoad() {
super.viewDidLoad()
guard let img = UIImage(named: "myImage") else {
fatalError("Could not load the image!!!")
}
scrollView = UIScrollView()
imageView = UIImageView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleToFill
scrollView.addSubview(imageView)
view.addSubview(scrollView)
// respect safe area
let g = view.safeAreaLayoutGuide
imageViewTopConstraint = imageView.topAnchor.constraint(equalTo: scrollView.topAnchor)
imageViewBottomConstraint = imageView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor)
imageViewLeadingConstraint = imageView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor)
imageViewTrailingConstraint = imageView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor)
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: g.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
imageViewTopConstraint,
imageViewBottomConstraint,
imageViewLeadingConstraint,
imageViewTrailingConstraint,
])
scrollView.delegate = self
scrollView.minimumZoomScale = 0.1
scrollView.maximumZoomScale = 5.0
imageView.image = img
imageView.frame.size = img.size
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { _ in
self.updateMinZoomScaleForSize(size, shouldSize: (self.scrollView.zoomScale == self.scrollView.minimumZoomScale))
self.updateConstraintsForSize(size)
}, completion: {
_ in
})
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
updateMinZoomScaleForSize(scrollView.bounds.size)
updateConstraintsForSize(scrollView.bounds.size)
if fitMode == .scaleAspectFill {
centerImageView()
}
}
func updateMinZoomScaleForSize(_ size: CGSize, shouldSize: Bool = true) {
guard let img = imageView.image else {
return
}
var bShouldSize = shouldSize
let widthScale = size.width / img.size.width
let heightScale = size.height / img.size.height
var minScale = min(widthScale, heightScale)
let startScale = max(widthScale, heightScale)
if fitMode == .scaleAspectFill && !allowFullImage {
minScale = startScale
}
if scrollView.zoomScale < minScale {
bShouldSize = true
}
scrollView.minimumZoomScale = minScale
if bShouldSize {
scrollView.zoomScale = fitMode == .scaleAspectFill ? startScale : minScale
}
}
func scrollViewDidZoom(_ scrollView: UIScrollView) {
updateConstraintsForSize(scrollView.bounds.size)
}
func centerImageView() -> Void {
let yOffset = (scrollView.frame.size.height - imageView.frame.size.height) / 2
let xOffset = (scrollView.frame.size.width - imageView.frame.size.width) / 2
scrollView.contentOffset = CGPoint(x: -xOffset, y: -yOffset)
}
func updateConstraintsForSize(_ size: CGSize) {
let yOffset = max(0, (size.height - imageView.frame.height) / 2)
imageViewTopConstraint.constant = yOffset
imageViewBottomConstraint.constant = yOffset
let xOffset = max(0, (size.width - imageView.frame.width) / 2)
imageViewLeadingConstraint.constant = xOffset
imageViewTrailingConstraint.constant = xOffset
view.layoutIfNeeded()
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return imageView
}
}
Edit
As an example, I used this image (2560 x 1440):
and I get this result on launch:
and maximum zoom in (5.0) scrolled to top-center:
Edit 2
Same image, at launch, with:
var fitMode: UIView.ContentMode = .scaleAspectFill
instead of .scaleAspectFit:
I've found this solution that works for me, when setting and changing the image, I calculate the minimum needed zoom scale and set it on the scrollView:
var selectedMapImage: MapImage? {
didSet {
mapImageView.image = mapImagesController.getImageForMapImage(selectedMapImage!)
mapImageScrollView.minimumZoomScale = view.bounds.height / mapImageView.image!.size.height
mapImageScrollView.setZoomScale(mapImageScrollView.minimumZoomScale, animated: true)
mapImageScrollView.scrollRectToVisible(view.bounds, animated: true)
}
}

How to adjust height of child view controller to match container view's height

I have a container View with adjustable height. Now I want to set the same frame of the view of child view controller according to container view's height. Sometimes it does work in the simulation but often it fails. The frame of child view controller's view often stays the same as defined at the beginning. Do you know a workaround to solve this problem ?
Here is my main ViewController with containerView named bottomView
class ViewController: UIViewController {
var startPosition: CGPoint!
var originalHeight: CGFloat = 0
var bottomView = UIView()
var gestureRecognizer = UIPanGestureRecognizer()
let scrollView = UIScrollView()
let controller = EditorViewController()
override func viewDidLoad() {
self.view.backgroundColor = .white
view.addSubview(bottomView)
bottomView.translatesAutoresizingMaskIntoConstraints = false
bottomView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
bottomView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
bottomView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
bottomView.heightAnchor.constraint(equalToConstant: 100).isActive = true
bottomView.backgroundColor = .white
gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(viewDidDragged(_:)))
bottomView.isUserInteractionEnabled = true
bottomView.addGestureRecognizer(gestureRecognizer)
// add childviewController
self.addChild(controller)
controller.view.translatesAutoresizingMaskIntoConstraints = false
controller.view.frame = bottomView.bounds
bottomView.addSubview(controller.view)
controller.view.rightAnchor.constraint(equalTo: bottomView.rightAnchor).isActive = true
controller.view.leftAnchor.constraint(equalTo: bottomView.leftAnchor).isActive = true
controller.view.bottomAnchor.constraint(equalTo: bottomView.bottomAnchor).isActive = true
controller.view.topAnchor.constraint(equalTo: bottomView.topAnchor).isActive = true
controller.didMove(toParent: self)
}
}
The childView Controller looks like this:
class EditorViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .red
}
And in this way I change the height of container view and tried to adjust the height of child viewController, too. But it does not work.
#objc func viewDidDragged(_ sender: UIPanGestureRecognizer) {
if sender.state == .began {
startPosition = gestureRecognizer.location(in: self.view) // the postion at which PanGestue Started
originalHeight = bottomView.frame.size.height
}
if sender.state == .began || sender.state == .changed {
let endPosition = sender.location(in: self.view)
let difference = endPosition.y - startPosition.y
let newHeight = originalHeight - difference
if self.view.frame.size.height - endPosition.y < 100 {
bottomView.frame = CGRect(x: 0, y: self.view.frame.size.height - 100, width: self.view.frame.size.width, height: 100)
controller.view.frame = bottomView.bounds
} else {
bottomView.frame = CGRect(x: 0, y: self.view.frame.size.height - newHeight, width: self.view.frame.size.width, height: newHeight)
controller.view.frame = bottomView.bounds
}
}
if sender.state == .ended || sender.state == .cancelled {
//Do Something
}
}
Try using a constraint to change the height. Something like:
class ViewController: UIViewController {
...
var heightConstraint: NSLayoutConstraint?
override func viewDidLoad() {
...
heightConstraint = bottomView.heightAnchor.constraint(equalToConstant: 100)
heightConstraint?.isActive = true
....
}
#objc func viewDidDragged(_ sender: UIPanGestureRecognizer) {
...
if sender.state == .began || sender.state == .changed {
let endPosition = sender.location(in: self.view)
let difference = endPosition.y - startPosition.y
let newHeight = originalHeight - difference
if self.view.frame.size.height - endPosition.y < 100 {
heightConstraint?.constant = 100
} else {
heightConstraint?.constant = newHeight
}
}
...
}
}

Swift - how to apply resize images inside scrollview

I have a following scrollview code
class ViewController: UIViewController, UIScrollViewDelegate {
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var pageControl: UIPageControl!
var imageArray = [UIImage(named: "mainScrollView1"), UIImage(named: "mainScrollView2"), UIImage(named: "mainScrollView3"),]
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
configurePageControl()
scrollView.pagingEnabled = true
scrollView.showsHorizontalScrollIndicator = false
scrollView.delegate = self
for var i = 0; i < imageArray.count; i++ {
if i == 0 {
var imageView = UIImageView(frame: CGRectMake(0, 0, scrollView.frame.size.width, 160))
imageView.image = imageArray[i]
scrollView.addSubview(imageView)
} else {
var float: CGFloat = CGFloat(i)
var imageView = UIImageView(frame: CGRectMake(scrollView.frame.size.width * float, 0, scrollView.frame.size.width, 160))
imageView.image = imageArray[i]
scrollView.addSubview(imageView)
}
}
scrollView.contentSize = CGSizeMake(scrollView.frame.size.width*3, scrollView.frame.size.height)
}
func configurePageControl() {
self.pageControl.numberOfPages = 3
self.pageControl.currentPage = 0
self.pageControl.tintColor = UIColor.redColor()
self.pageControl.pageIndicatorTintColor = UIColor.blackColor()
self.pageControl.currentPageIndicatorTintColor = UIColor.whiteColor()
self.view.addSubview(pageControl)
}
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
let pageNumber = round(scrollView.contentOffset.x / scrollView.frame.size.width)
pageControl.currentPage = Int(pageNumber)
}
}
the problem is that images inside scroll a static ones, they do not change their size for different screen sizes and because of that it looks like this
as you see first image do not expands to fill whole screen width and because of this part of second image can be seen
How can this be fixed ?
You should update the image size after the view layout all subviews. You should do that in viewDidLayoutSubviews.
Something like this:
var imageArray = [UIImage(named: "mainScrollView1"), UIImage(named: "mainScrollView2"), UIImage(named: "mainScrollView3"),]
var imageViews:[UIImageView] = []
override func viewDidLoad() {
super.viewDidLoad()
configurePageControl()
scrollView.pagingEnabled = true
scrollView.showsHorizontalScrollIndicator = false
scrollView.delegate = self
for image in imageArray {
var imageView = UIImageView(image: image)
scrollView.addSubview(imageView)
imageViews.append(imageView)
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
for (index,imageView) in imageViews.enumerate() {
imageView.frame = CGRectMake(CGFloat(index)*scrollView.frame.size.width, 0, scrollView.frame.size.width, 160)
}
scrollView.contentSize = CGSizeMake(scrollView.frame.size.width*3, scrollView.frame.size.height)
}