I am trying to create a full page sized horizontally scrolling UIScrollView. On each page I am adding instances of the same UIViewController class. I would like to create some kind of reusable functionality to conserve both memory and processor use needed. Below is a basic implementation I have created with some toying around with how reusability might work although Im not quite sure. Thank you for any help you can offer.
Current UIScroll ViewController Model
let scrollView:UIScrollView = {
let scrollView = UIScrollView(frame: CGRect.zero)
scrollView.isPagingEnabled = true
scrollView.backgroundColor = UIColor.gray
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = true
return scrollView
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
scrollView.frame = CGRect(origin: CGPoint(x: 0, y: 0), size: self.view.bounds.size)
self.view.addSubview(scrollView)
scrollView.delegate = self
scrollView.contentSize = CGSize(width: 3 * self.view.frame.width, height: self.view.frame.height)
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 0),
scrollView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 0),
scrollView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 0),
scrollView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: 0)
])
let viewController1 = UIViewController()
viewController1.view.backgroundColor = UIColor.red
let viewController2 = UIViewController()
viewController2.view.backgroundColor = UIColor.blue
let viewController3 = UIViewController()
viewController3.view.backgroundColor = UIColor.green
self.addChild(viewController1)
viewController1.view.frame = CGRect(origin: CGPoint(x: 0, y: 0), size: self.view.bounds.size)
scrollView.addSubview(viewController1.view)
addViewControllerContraints(viewController: viewController1, index: 0)
viewController1.didMove(toParent: self)
self.addChild(viewController2)
viewController2.view.frame = CGRect(origin: CGPoint(x: self.view.bounds.width, y: 0), size: self.view.bounds.size)
scrollView.addSubview(viewController2.view)
addViewControllerContraints(viewController: viewController2, index: 1)
viewController2.didMove(toParent: self)
self.addChild(viewController3)
viewController3.view.frame = CGRect(origin: CGPoint(x: 2 * self.view.bounds.width, y: 0), size: self.view.bounds.size)
scrollView.addSubview(viewController3.view)
addViewControllerContraints(viewController: viewController3, index: 2)
viewController3.didMove(toParent: self)
}
func addViewControllerContraints( viewController: UIViewController, index:Int){
guard let view = viewController.view else{
print("View found nil")
return
}
view.translatesAutoresizingMaskIntoConstraints = false
let offset:CGFloat = UIScreen.main.bounds.width * CGFloat(index)
print("Offset: \(offset)")
NSLayoutConstraint.activate([
view.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 0),
view.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: offset),
view.heightAnchor.constraint(equalToConstant: self.view.bounds.height),
view.widthAnchor.constraint(equalToConstant: self.view.bounds.width)
])
}
Is there a good way to create some type of reuse functionality this is something I was playing around with based on This Answer although I realize that is primarily for UIPageViewControllers where allocation and deallocation of UIViewController's is handled for you.
Possible Reuse Functionality
var reuseableViewControllers:[UIViewController] = [UIViewController]()
private func unusedViewController() -> UIViewController {
let unusedViewControllers = reuseableViewControllers.filter { $0.parent == nil }
if let someUnusedViewController = unusedViewControllers.first {
return someUnusedViewController
} else {
let newViewController = UIViewController()
reuseableViewControllers.append(newViewController)
return newViewController
}
}
Related
For some reason scrollView.zoom does not zoom in on an imageView.
I have an imageView inside a scrollView.
in ViewDidLoad:
scrollView.minimumZoomScale = 1.0
scrollView.maximumZoomScale = 2.0
scrollView.delegate = self
viewForZooming:
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return imageView
}
Now, I'm calling below after both scrollView and imageView are initialized.
var scale: CGFloat = 2
let location = CGPoint(x: imageView.frame.width/2, y: imageView.frame.height/2)
scrollView.zoom(to: zoomRectForScale(scale, center: location), animated: true)
Nothing is happening after scrollView.zoom is called. I tried doing
view.setNeedsDisplay()
view.layoutIfNeeded()
Still nothing happens, imageView is not zooming in.
UPDATE:
As requested, here is the code for scrollView and imageView initialization:
func createscrollView(image: UIImage?){
if image == nil{
let img = UIImage(named: "demo image.jpg")
let imgCorrected = scaleAndOrient(image: img!)
//created user selecged images
userSelectedImage_UI = img
userSelectedImage_UI_Corrected = imgCorrected
}
// create image scroll view
scrollView = UIScrollView(frame: CGRect(x: 0, y: 0,width: 100, height: 100))
scrollView.showsHorizontalScrollIndicator = false
scrollView.showsVerticalScrollIndicator = false
scrollView.bouncesZoom = false
scrollView.bounces = false
scrollView.contentMode = .scaleAspectFill
view.addSubview(scrollView)
scrollView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 0),
scrollView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 0),
scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -150),
scrollView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: 0)
])
// add image view to scrollview
imageView = UIImageView(frame: CGRect(x: 0, y: 0,width: 100, height: 100))
scrollView.addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor, constant: 0),
imageView.leftAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leftAnchor, constant: 0),
imageView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor, constant: 0),
imageView.rightAnchor.constraint(equalTo: scrollView.contentLayoutGuide.rightAnchor, constant: 0),
imageView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, multiplier: 1),
imageView.heightAnchor.constraint(equalTo: scrollView.heightAnchor, multiplier: 1)
])
imageView.contentMode = .scaleAspectFit
if image == nil{
imageView.image = userSelectedImage_UI_Corrected
} else {
imageView.image = scaleAndOrient(image: image!)
}
}
Based on comments...
It sounds like you are creating / setting up your scrollView and its content imageView from viewDidLoad() and then immediately trying to "zoom" it.
If so, that will be problematic, as auto-layout hasn't finished laying out the UI elements.
You can call it from:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
var scale: CGFloat = 2
let location = CGPoint(x: imageView.frame.width/2, y: imageView.frame.height/2)
scrollView.zoom(to: zoomRectForScale(scale, center: location), animated: true)
}
I have a feeling the problem is in your zoomRectForScale function, though it isn't posted. It should be something like this:
func zoomRectForScale(_ scale: CGFloat, center: CGPoint) -> CGRect {
var zoomRect = CGRect.zero
zoomRect.size.height = imageView.frame.size.height / scale
zoomRect.size.width = imageView.frame.size.width / scale
let newCenter = scrollView.convert(center, from: imageView)
zoomRect.origin.x = newCenter.x - (zoomRect.size.width / 2.0)
zoomRect.origin.y = newCenter.y - (zoomRect.size.height / 2.0)
return zoomRect
}
Additionally, I've verified that double-tapping to zoom works to the correct location using:
var gestureRecognizer: UITapGestureRecognizer!
// Sets up the gesture recognizer that receives double taps to auto-zoom
func setupGestureRecognizer() {
gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap))
gestureRecognizer.numberOfTapsRequired = 2
scrollView.addGestureRecognizer(gestureRecognizer)
}
// Handles a double tap by either resetting the zoom or zooming to where was tapped
#IBAction func handleDoubleTap() {
if scrollView.zoomScale == 1 {
scrollView.zoom(to: zoomRectForScale(scrollView.maximumZoomScale, center: gestureRecognizer.location(in: gestureRecognizer.view)), animated: true)
} else {
scrollView.setZoomScale(1, animated: true)
}
}
Don't forget to call this in viewDidLoad:
setupGestureRecognizer()
If this doesn't solve your issue, please adjust your question with missing code functions and variable declarations.
I have spent a while trying to figure this out, but I just can't. There is probably something really simple I am missing here. I am trying to animate a view coming in from the bottom. This si the code I am using for the view:
private let undoView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .white
view.layer.cornerRadius = 15
view.layer.shadowOffset = .zero
view.layer.shadowOpacity = 0.3
view.layer.shadowRadius = 5
view.layer.shouldRasterize = true
view.layer.rasterizationScale = UIScreen.main.scale
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Undo", for: .normal)
button.titleLabel?.textAlignment = .center
button.setTitleColor(.systemBlue, for: .normal)
let buttonConstraints = [
button.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
button.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
button.topAnchor.constraint(equalTo: view.topAnchor, constant: 16),
button.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -16)
]
view.addSubview(button)
NSLayoutConstraint.activate(buttonConstraints)
let swipeGesture = UISwipeGestureRecognizer(target: view, action: #selector(undoViewSwiped))
view.addGestureRecognizer(swipeGesture)
return view
}()
And this is the code I am using to try and achieve the animation:
func addUndoView() {
var bottomConstraint = undoView.topAnchor.constraint(equalTo: view.bottomAnchor, constant: 0)
let constraints = [
undoView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
undoView.widthAnchor.constraint(equalToConstant: view.bounds.width / 3),
bottomConstraint,
undoView.heightAnchor.constraint(equalToConstant: 50)
]
view.addSubview(undoView)
NSLayoutConstraint.activate(constraints)
undoView.layoutIfNeeded()
bottomConstraint.isActive = false
bottomConstraint = undoView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor, constant: -58)
bottomConstraint.isActive = true
UIView.animate(withDuration: 0.5, delay: 0.2, animations: {
self.undoView.layoutIfNeeded()
})
}
I want the view to just be created underneath the visible view, and then slide up to 8 above the bottom layout margin. This code however just makes the view 'expand' at its final position instead of coming into view from the bottom. Using self.view.layoutIfNeeded() makes the view fly in from the top left of the screen, for some reason.
good day I usually work with transform property for move one view from x position to y portion, please check this example.
class ViewController: UIViewController {
let button : UIButton = {
let button = UIButton(type: .custom)
button.setTitle("Button", for: .normal)
button.backgroundColor = .gray
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
let customView : UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .red
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
view.addSubview(button)
view.addSubview(customView)
NSLayoutConstraint.activate([
button.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
button.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
customView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
customView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
customView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
customView.heightAnchor.constraint(equalToConstant: 100)
])
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
setupAnimationCustomView()
}
private func setupAnimationCustomView(){
UIView.animate(withDuration: 1, delay: 0 , options: .curveEaseOut, animations: {
print(self.button.frame.origin.y)
let translationY = self.view.frame.height - self.button.frame.origin.y - self.button.frame.height - self.customView.frame.height - 8
self.customView.transform = CGAffineTransform(translationX: 0, y: translationY * (-1))
}, completion: nil)
}
}
on the animation I calculate the distance that I need to move my customView.
I've got a view controller that has a UIView (featuredStoryView) and inside that view, I've added a scroll view (scrollView) and inside the scroll view, I've added an ImageView (bookCover), one on top of the other. Here is the code for all of those elements:
func setupFeaturedStoryView() {
featuredStoryView.backgroundColor = .white
featuredStoryView.layer.cornerRadius = 12
// Let the user tap on the featured story image view
featuredStoryView.isUserInteractionEnabled = true
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(featuredStoryViewTapped))
featuredStoryView.addGestureRecognizer(tapGesture)
view.addSubview(featuredStoryView)
addFeaturedStoryViewConstraints()
}
func setupScrollView() {
scrollView.backgroundColor = UIColor(red: 10/255, green: 10/255, blue: 20/255, alpha: 0.5)
scrollView.layer.cornerRadius = 15
featuredStoryView.addSubview(scrollView)
// Add the constraints to the scroll view
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.topAnchor.constraint(equalTo: featuredStoryView.topAnchor).isActive = true
scrollView.leadingAnchor.constraint(equalTo: featuredStoryView.leadingAnchor).isActive = true
scrollView.trailingAnchor.constraint(equalTo: featuredStoryView.trailingAnchor).isActive = true
scrollView.bottomAnchor.constraint(equalTo: featuredStoryView.bottomAnchor).isActive = true
}
func setupBookCover() {
bookCover.backgroundColor = .yellow
bookCover.layer.cornerRadius = 15
bookCover.contentMode = .scaleAspectFill
scrollView.addSubview(bookCover)
addBookCoverConstraints()
}
// Add the constraints to the featured story view
func addFeaturedStoryViewConstraints() {
featuredStoryView.translatesAutoresizingMaskIntoConstraints = false
featuredStoryView.topAnchor.constraint(equalTo: view.centerYAnchor, constant: -130).isActive = true
featuredStoryView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 100).isActive = true
featuredStoryView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -100).isActive = true
featuredStoryView.bottomAnchor.constraint(equalTo: view.centerYAnchor, constant: 130).isActive = true
}
// Add constraints to the book cover
func addBookCoverConstraints() {
bookCover.translatesAutoresizingMaskIntoConstraints = false
bookCover.topAnchor.constraint(equalTo: featuredStoryView.topAnchor).isActive = true
bookCover.leadingAnchor.constraint(equalTo: featuredStoryView.leadingAnchor).isActive = true
bookCover.trailingAnchor.constraint(equalTo: featuredStoryView.trailingAnchor).isActive = true
bookCover.bottomAnchor.constraint(equalTo: featuredStoryView.bottomAnchor).isActive = true
}
When you tap on the featuredStoryView, the following code snippet runs:
#objc func featuredStoryViewTapped() {
scrollView.contentSize.height = 1500
let animator = UIViewPropertyAnimator(duration: 0.6, dampingRatio: 0.8) {
self.featuredStoryView.frame = CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height)
self.scrollView.frame = CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height)
self.view.layoutIfNeeded()
self.bookCover.frame = CGRect(x: self.featuredStoryView.frame.midX - (bookCoverWidth / 2), y: 90, width: bookCoverWidth, height: bookCoverHeight)
self.view.layoutIfNeeded()
}
animator.startAnimation()
}
Now take a look at this Gif:
The problem here is, when I scroll, the yellow imageView(bookCover) moves to the top left corner, why does this happen? It should just go up with the scroll view. Is there any way to fix this?
You can not set frame directly like self.featuredStoryView.frame = CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height) and contentSize like scrollView.contentSize.height = 1500 in auto layout. So try to set constraints as per your requirement.
I have been making search bars without navigation on the imageView.
The search bar height is fixed but i want to change the search bar height.
so i tried
let frame = CGRect(x: 0, y: 0, width: 100, height: 44)
searchbar.frame = frame
and
searchbar.heightAnchor.constraint(equalToConstant: 200).isActive = true
but they don't work.
I'm using this code
searchBar.isTranslucent = true
searchBar.searchBarStyle = .minimal
so like this
please help me change the search bar textfield height.
fileprivate func customizeSearchField(){
UISearchBar.appearance().setSearchFieldBackgroundImage(UIImage(), for: .normal)
searchField.backgroundColor = .white
if let searchTextField = searchField.value(forKey: "searchField") as? UITextField {
searchTextField.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
searchTextField.heightAnchor.constraint(equalToConstant: 50),
searchTextField.leadingAnchor.constraint(equalTo: searchField.leadingAnchor, constant: 10),
searchTextField.trailingAnchor.constraint(equalTo: searchField.trailingAnchor, constant: -10),
searchTextField.centerYAnchor.constraint(equalTo: searchField.centerYAnchor, constant: 0)
])
searchTextField.clipsToBounds = true
searchTextField.font = GenericUtility.getOpenSansRegularFontSize(14)
searchTextField.layer.cornerRadius = 4.0
searchTextField.layer.borderWidth = 1.0
searchTextField.layer.borderColor = AppColor.primaryLightGray.cgColor
}
}
try this!
for myView in searchBars.subviews {
for mySubView in myView.subviews {
if let textField = mySubView as? UITextField {
var bounds: CGRect
bounds = textField.frame
bounds.size.height = 40 //(set your height)
textField.bounds = bounds
textField.borderStyle = UITextBorderStyle.RoundedRect
}
}
}
Try this:
searchBar.frame.size.height = 44
if you want to use with interface builder:
class MediaSearchBar: UISearchBar {
override func layoutSubviews() {
}
}
and setup it in viewDidLoad:
func setupSearchBar() {
searchBar.delegate = self
for myView in searchBar.subviews {
for mySubView in myView.subviews {
if let textField = mySubView as? UITextField {
textField.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
textField.heightAnchor.constraint(equalTo: myView.heightAnchor,
multiplier: 1.0, constant: -20.0),
textField.leadingAnchor.constraint(equalTo: myView.leadingAnchor, constant: 10.0),
textField.trailingAnchor.constraint(equalTo: myView.trailingAnchor, constant: -10.0),
textField.centerYAnchor.constraint(equalTo: myView.centerYAnchor, constant: 0.0)
])
textField.clipsToBounds = true
}
}
}
}
I have an iOS 10 app that contains a button ("Press Me!"), and a UIView containing a blue outlined rectangle, with no fill. I want to fill the rectangle gradually (in like 5 seconds) with red, using any possible method (I tried using UIView.animation(withDuration:, animations:)). I will include the code below:
Here's the viewController:
class ViewController: UIViewController {
#IBOutlet weak var tronkyy: tronc!
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func didPress(_ sender: UIButton) {
tronkyy.full = true
tronkyy.setNeedsDisplay()
} }
And the view:
#IBDesignable class tronc: UIView {
//var value = 0.0
var full = false
override func draw(_ rect: CGRect) {
UIColor.blue.setStroke()
boxPath().stroke()
drawDynamic()
}
private func boxPath() -> UIBezierPath {
let path = UIBezierPath(rect: CGRect(x: 0.1 * frame.width, y: 0.1 * frame.height, width: 0.8 * frame.width, height: 0.8 * frame.height))
UIColor.blue.setStroke()
return path
}
func drawDynamic() {
if full {
UIView.animate(withDuration: 10) { _ in
//while self.value < 1 {
let path = UIBezierPath(rect: CGRect(x: 0.1 * self.frame.width, y: 0.1 * self.frame.height, width: (0.8 * self.frame.width), height: 0.8 * self.frame.height)) //* CGFloat(value)))
UIColor.red.setFill()
path.fill()
//self.value += 0.1
//}
}
full = false
}
} }
Ah, and yes, please ignore that the red view cover the rectangle.
The problem is that there is no gradual filling. it just fills it once and for all.
Edit: I commented out the "value" property and the while loop, as suggested in the answers, but it still doesn't work.
This is an approach using a view with a subview. For a more complex shape than a rectangle, it could be done with masks.
You can run this directly in a Playground page:
import UIKit
import PlaygroundSupport
class TestViewController : UIViewController {
let btn: UIButton = {
let b = UIButton()
b.setTitle("Tap Me", for: .normal)
b.backgroundColor = .blue
b.frame = CGRect(x: 20, y: 20, width: 200, height: 30)
return b
}()
let blueRect: UIView = {
let v = UIView()
v.layer.borderColor = UIColor.blue.cgColor
v.layer.borderWidth = 2.0
v.backgroundColor = .clear
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let redRect: UIView = {
let v = UIView()
v.backgroundColor = .red
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
var redRectHeightConstraint: NSLayoutConstraint?
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(btn)
btn.addTarget(self, action: #selector(didTap(_:)), for: .touchUpInside)
blueRect.addSubview(redRect)
self.view.addSubview(blueRect)
redRect.leftAnchor.constraint(equalTo: blueRect.leftAnchor, constant: 0.0).isActive = true
redRect.rightAnchor.constraint(equalTo: blueRect.rightAnchor, constant: 0.0).isActive = true
redRect.bottomAnchor.constraint(equalTo: blueRect.bottomAnchor, constant: 0.0).isActive = true
redRectHeightConstraint = NSLayoutConstraint(item: redRect,
attribute: .height,
relatedBy: .equal,
toItem: nil,
attribute: .notAnAttribute,
multiplier: 1.0,
constant: 0.0)
redRect.addConstraint(redRectHeightConstraint!)
blueRect.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 0.5).isActive = true
blueRect.heightAnchor.constraint(equalTo: self.view.heightAnchor, multiplier: 0.5).isActive = true
blueRect.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
blueRect.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
}
func didTap(_ sender: Any?) -> Void {
UIView.animate(withDuration: 5.0, animations: {
_ in
self.redRectHeightConstraint!.constant = self.blueRect.frame.size.height
self.view.setNeedsLayout()
}, completion: {
finished in
})
}
}
let vc = TestViewController()
vc.view.backgroundColor = .yellow
vc.view.frame = CGRect(x: 0, y: 0, width: 600, height: 600)
PlaygroundPage.current.liveView = vc
Note: this is just sample, demonstration code... not meant to be a copy/paste solution.
You should not use a while loop in an animation function. Try this version
UIView.animate(withDuration: 10) { _ in
let path = UIBezierPath(rect: CGRect(x: 0.1 * self.frame.width, y: 0.1 * self.frame.height, width: (0.8 * self.frame.width), height: 0.8 * self.frame.height))
UIColor.red.setFill()
path.fill()
}