Why doesn't hitTest(_:with:) interact with programmatically added UIImageView - ios

I'm having trouble getting hitTest(_:with:) to traverse a UIImageView when it's added programmatically.
In IB I make a square area and give it a tag of 1. Within that square I insert a UIImageView (tag=2) (orange/white in screenshot below). In the ViewController I programmatically create another square(tag=3). Then add a UIImageView(tag=4) within that (red/green in screenshot below). The following picture shows this:
I'm using touchesBegan(_:with:) to perform hitTest. When I touch the orange I get a printout of a view with tag 1 (expected). If I touch the white square I get printout with tag 2 (expected). These were created in IB.
When I touch the red square I get tag 3 (expected). When I touch green I get tag 3! (NOT expected). I've looked at the runtime hierarchy and don't see any difference in structure between IB & programmatic. It appears that hitTest(_:with:) doesn't recognize/traverse/include/detect the programmatic UIImageView. What's the difference?
Here's my ViewController code:
import UIKit
class ViewController: UIViewController {
#IBOutlet var gameView: UIView!
#IBOutlet var boardView: UIImageView!
var g2 : UIView!
var b2 : UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
g2 = UIView(frame: CGRect(x: 100, y: 50, width: 240, height: 240))
g2.backgroundColor = .red
g2.tag = 3
view.addSubview(g2)
b2 = UIImageView(frame: CGRect(x: 75, y: 75, width: 80, height: 80))
b2.backgroundColor = .green
b2.tag = 4
g2.addSubview(b2)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if event!.type != .touches { return }
if let first = touches.first {
let loc = first.location(in: view)
if let v = view.hitTest(loc, with: nil) {
print("\(v)")
}
}
}
}
I've changed the programmatic UIImageView to a UIView and that IS detected by hitTest as expected.

UIImageView has isUserInteractionEnabled set to false by default, e.g. it doesn't receive any touches.
This should be true for a storyboard too, probably you have enabled User Interactions Enabled checkmark.
Most of other views have this property set to true by default, but if you wanna interact with UIImageView touches, you need to enable it explicitly.
b2.isUserInteractionEnabled = true

Related

Paging with ScrollView using directly storyboard instead of creating .xib files

Sorry in advance, this is a long question but I wanted to explain as good as I can.
I need to implement a walkthrough(first time launch guide) for the project, which will only be shown to user for the first time they launch the app.
I've implemented it by creating a .xib file, where I was just creating a view for each item I need to implement for the walkthrough by creating objects referring to .xib model.
But now I'm required to implement it without using .xib files, where I need to do it via storyboard. This time I've inserted a scrollView, and then I put another view inside it with the objects I'm going to need such as (labels,imageViews,buttons etc.) But it doesn't work, even though I can create all the objects by copying the view right under the scrollView. When I try to expand the width of the scrollView with all the views created so I can do paging, it doesn't create each view next to each other but only shows me the last object(view) in the simulator as they add up vertically, and I can't do no paging.
Now I provide some sample code to show how I try to implement this:
import UIKit
class DenemeViewController: UIViewController, UIScrollViewDelegate {
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var pageControl: UIPageControl!
#IBOutlet weak var repeatingView: UIView!
var viewArray = [UIView]()
override func viewDidLoad() {
super.viewDidLoad()
let temporaryView = repeatingView
temporaryView?.backgroundColor = .black
viewArray.append(temporaryView!)
let temporaryViewTwo = repeatingView
temporaryViewTwo?.backgroundColor = .blue
viewArray.append(temporaryViewTwo!)
pageControl.numberOfPages = viewArray.count
pageControl.currentPage = 0
view.bringSubview(toFront: pageControl)
setupScrollView(items: viewArray)
}
func setupScrollView(items: [UIView]) {
scrollView.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height)
scrollView.contentSize = CGSize(width: view.frame.width * CGFloat(items.count), height: view.frame.height)
scrollView.isPagingEnabled = true
for i in 0..<items.count {
items[i].frame = CGRect(x: view.frame.width * CGFloat(i), y: 0, width: view.frame.width, height: view.frame.height)
scrollView.addSubview(items[i])
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let pageIndex = round(scrollView.contentOffset.x/view.frame.width)
pageControl.currentPage = Int(pageIndex)
}
}
As you can see I first create two different views in viewDidLoad by referring to "repeatingView" which is the outlet of the view inside scrollView on the storyboard. Then I return them in an array and try to implement scrollView's subviews with views.
I was expecting to see the scrollView starting with the view with black background and as I do the paging through right then the view with blue background should appear.
When I run this what I see is only the blue view, and I'm unable to do any paging or scrolling.

Swift 3 Horizontal Multiple UIScrollView - Different Speeds

I have created two UIScrollViews ( One named scrollView and one named scrollLevel4 )
when I move scrollLevel4, I can get scrollView to move at the same speed using :-
func scrollViewDidScroll(_ scrollLevel4: UIScrollView) {
scrollView.contentOffset.x = scrollLevel4.contentOffset.x
}
However If I want to move scrollView at a different pace, not sure what to do, whenever I add anything to the end of line :
scrollView.contentOffset.x = scrollLevel4.contentOffset.x
it crashes, even a simple + 10, same pace, staggered offset, still crashes
also tried .scrollRectToVisible() method
Thoughts ?
Without seeing the error or your code it's hard to say for sure, but most likely you are setting the same delegate for both scrollViews. When you drag scrollLevel4, it triggers a scroll on scrollView, so you get an infinite loop and eventually a crash.
If you want to use the same delegate on both scrollViews, you'll need to check which one was passed before operating on them. Here's a basic working implementation. Open a new single view project and replace the code in ViewController.swift with:
import UIKit
class ViewController: UIViewController, UIScrollViewDelegate {
var imageView1: UIImageView!
var imageView2: UIImageView!
var scrollView1: UIScrollView!
var scrollView2: UIScrollView!
override func viewDidLoad() {
super.viewDidLoad()
imageView1 = UIImageView(image: UIImage(named: "image.jpg"))
imageView2 = UIImageView(image: UIImage(named: "image.jpg"))
scrollView1 = UIScrollView(frame: CGRect(origin: CGPoint.zero, size: CGSize(width: 200, height: 200)))
scrollView1.contentSize = imageView1.bounds.size
scrollView1.addSubview(imageView1)
view.addSubview(scrollView1)
scrollView2 = UIScrollView(frame: CGRect(origin: CGPoint(x: 0, y: 210), size: CGSize(width: 200, height: 200)))
scrollView2.contentSize = imageView2.bounds.size
scrollView2.addSubview(imageView2)
view.addSubview(scrollView2)
scrollView2.delegate = self
scrollView1.delegate = self
}
func scrollViewDidScroll(_ scrolled: UIScrollView) {
// both scrollViews call this when scrolled, so determine which was scrolled before adjusting
if scrolled === scrollView1 {
scrollView2.contentOffset.x = scrolled.contentOffset.x + 100
} else if scrolled === scrollView2 {
scrollView1.contentOffset.x = scrolled.contentOffset.x - 100
}
}
}
Note that whatever modification you apply to the offset of one, you have to apply the exact inverse (or nothing at all) to the other. Otherwise you'll have an infinite loop of back and forth scrolling ending in a crash.

Move label position in swift with a gesture

I have a label in my view controller. I am trying to move the position of the label 50 points to the left every time I tap the label. This is what I have so far but my label won't move in the simulation. I do have constraints on the label. It is about 100 wide and 50 in height, and it is also centered in the view controller.
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let tapGesture = UITapGestureRecognizer(target: gestureLabel, action: #selector(moveLabel))
gestureLabel.addGestureRecognizer(tapGesture)
}
func moveLabel(){
if(left){
moveLeft()
}
else{
moveRight()
}
}
func moveLeft(){
let originalX = gestureLabel.frame.origin.x
if(originalX < 0){
bringIntoFrame()
}
else{
gestureLabel.frame.offsetBy(dx: -50, dy: 0)
}
}
here You can move label where ever You want it
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let position = touch.location(in: self.view)
print(position.x)
print(position.y)
lbl.frame.origin = CGPoint(x:position.x-60,y:position.y-50)
}
}
UILabel's userInteractionEnabled is false by default. Try setting it to true
You should be changing the constraint of the label, not the label itself.
Also, your target should be self, not gestureLabel.
Plus, you could animate that. :)
Like so:
class ViewController: UIViewController {
// Centers the Meme horizontally.
#IBOutlet weak var centerHorizontalConstraint: NSLayoutConstraint!
// A custom UIView.
#IBOutlet weak var okayMeme: OkayMeme!
override func viewDidLoad() {
super.viewDidLoad()
addGestureRecognizer()
}
func addGestureRecognizer() {
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(moveMeme))
okayMeme.addGestureRecognizer(tapGesture)
}
func moveMeme() {
UIView.animate(withDuration: 2.0) {
self.centerHorizontalConstraint.constant -= 50
self.view.layoutIfNeeded()
}
}
}
"Demo":
You made a new frame, but didn't assign it to the label. offsetBy doesn't change the frame in place. It creates a new one.
Replace
gestureLabel.frame.offsetBy(dx: -50, dy: 0)
with
gestureLabel.frame = gestureLabel.frame.offsetBy(dx: -50, dy: 0)
But the method above assumes, that you're using directly, not constraints. A standard approach is to have a "Leading to Superview" constraint and change its constant instead of changing the frame.
The label wont move because it has constraints. The easiest way to do this is next:
1) create a label programatically (so you can move it freely around)
var labelDinamic = UILabel(frame: CGRectMake(0, 0,200, 200));
labelDinamic.text = "test text";
self.view.addSubview(labelDinamic);
2) set label initial position (i suggest to use the position of your current label that has constraints. also, hide you constraint label, because you dont need it to be displayed)
labelDinamic.frame.origin.x = yourLabelWithConstraints.frame.origin.x;
labelDinamic.frame.origin.y = yourLabelWithConstraints.frame.origin.y;
3) now you move your label where ever You want with the property
labelDinamic.frame.origin.x = 123
labelDinamic.frame.origin.y = 200

using gesture recognizer on a subview of UIScrollView - Swift

I have attached an image here
I am a newbie to iOS development. My question is as follows:
I want to swipe my scrollView outside of its width using gesture control.
To be precise I want my UIScrollView to scroll when swipe is performed in the subview (there is only one subview).
I have gone through several StackOverflow question but I couldn't quite get a correct answer.
Your help is much appreciated!
class ViewController: UIViewController, UIGestureRecognizerDelegate {
#IBOutlet weak var scrollView: UIScrollView!
var images = [UIImageView]()
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
var contentWidth: CGFloat = 0.0
for x in 0...2 {
let image = UIImage(named: "icon\(x).png")
let imageView = UIImageView(image:image)
images.append(imageView)
var newX: CGFloat = 0.0
newX = scrollView.frame.size.width/2 + scrollView.frame.size.width * CGFloat(x)
contentWidth += newX
scrollView.addSubview(imageView)
imageView.frame = CGRect(x: newX - 75, y: (scrollView.frame.size.height/2) - 75, width: 150, height: 150)
}
scrollView.clipsToBounds = false
scrollView.contentSize = CGSize(width: contentWidth, height: view.frame.size.height)
}
As you can see in the image the width of my UIScrollView has a grey background color (I did that to illustrate my point). Now I want to also scroll the scroll view when a user swipes in the subview (non-grey) UIImageView.
I have added the following function and made a return of yes to enable both views to recognize gestures simultaneously. But, I am not getting the desired result. Can anyone take a look?
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
Place view.addGestureRecognizer(scrollView.panGestureRecognizer) in viewDidLoad function.
What it eventually does is it assigns scrollview's gesture recognizer to look for gestures in main view. So it automatically move content of scrollview even when gestures are outside of the scrollview rect.
you can forward touch events in that subview to scrollview. see this answer

Checking background color of view in swift

Say for example I have a white background, then on every time the view is touched, a blue box sized (60x60) is added to the screen. If i keep tapping all over the screen until the entire view becomes blue, how can I use code to notify the user using for example an alert controller saying that the view has now changed color completely.
if that made sense to anyone, I would appreciate any suggestions :)
As others have stated, the best approach is to hold a reference to a 2D array to correlate with the UI. I would approach this by first:
1) Create a subclass of UIView called overlayView. This will be the view overlay on top of your View Controllers' view. This subclass will override init(frame: CGRect). You may implement a NxM grid on initialization. You will also create and add the subviews as appropriate.
Example:
let boxWidth: CGFloat = 60
let boxHeight: CGFloat = 60
let boxFrame: CGRect = CGRectMake(0, 0, 600, 600)
override init(frame: CGRect) {
super.init(frame: boxFrame)
for i in 1 ... 10 { //row
for j in 1...10 { //column
let xCoordinate: CGFloat = CGFloat(CGFloat(i) * boxWidth)
let yCoordinate: CGFloat = CGFloat(CGFloat(j) * boxHeight)
let boxFrame: CGRect = CGRect(x: xCoordinate, y: yCoordinate, width: boxWidth, height: boxHeight)
var subView: UIView = UIView(frame: boxFrame)
subView.backgroundColor = UIColor.greenColor()
self.addSubview(subView)
}
}
}
2) Override touchesBegan(touches: NSSet, withEvent event: UIEvent) within overlayView. The implementation should look something like this:
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
let touch: UITouch = touches.anyObject() as UITouch
let touchLocation: CGPoint = touch.locationInView(self)
for subView in self.subviews {
if(CGRectContainsPoint(subView.frame, touchLocation)) {
subView.removeFromSuperview()
}
}
}
2a) You will also want to create a function to notify your 2D array that this subview as been removed. This will be called from within your if statement. In addition, this function will detect if the array is empty (all values set to false)
OR
2b) if you do not need a reference to the model (you do not care which boxes are tapped only that they're all gone), you can skip 2a and check if self.subviews.count == 0. If this is the case, you do not need a 2D array at all. If this condition passes, you can present an alert to the user as needed.
3) Create an overlayView in your main View Controller and add it as a subview.

Resources