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.
Related
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
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
First of all I have checked almost every places over the internet but I didn't get any solution about this topic.
In my cases I have multiple UIView objects inside a superview or you can say a canvas where I am drawing this views.
All this views are attached with pan gesture so they can be moved inside anywhere of their superview.
Some of this views can be rotated using either rotation gesture or CGAffineTransformRotate.
Whenever any of the view will be outside of the main view then it will be deleted.
Now following are my code.
#IBOutlet weak var mainView: UIView!
var newViewToAdd = UIView()
newViewToAdd.layer.masksToBounds = true
var transForm = CGAffineTransformIdentity
transForm = CGAffineTransformScale(transForm, 0.8, 1)
transForm = CGAffineTransformRotate(transForm, CGFloat(M_PI_4)) //Making the transformation
newViewToAdd.layer.shouldRasterize = true //Making the view edges smooth after applying transforamtion
newViewToAdd.transform = transForm
self.mainView.addSubview(newViewToAdd) //Adding the view to the main view.
Now in case the gesture recognizer its inside the custom UIView Class -
var lastLocation: CGPoint = CGPointMake(0, 0)
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
self.superview?.bringSubviewToFront(self)
lastLocation = self.center //Getting the last center point of the view on first touch.
}
func detectPan(recognizer: UIPanGestureRecognizer){
let translation = recognizer.translationInView(self.superview!) //Making the translation
self.center = CGPointMake(lastLocation.x + translation.x, lastLocation.y + translation.y) //Updating the center point.
switch(recognizer.state){
case .Began:
break
case .Changed:
//MARK: - Checking The view is outside of the Superview or not
if (!CGRectEqualToRect(CGRectIntersection(self.superview!.bounds, self.frame), self.frame)) //if its true then view background color will be changed else background will be replaced.
{
self.backgroundColor = outsideTheViewColor
var imageViewBin : UIImageView
imageViewBin = UIImageView(frame:CGRectMake(0, 0, 20, 25));
imageViewBin.image = UIImage(named:"GarbageBin")
imageViewBin.center = CGPointMake(self.frame.width/2, self.frame.height/2)
addSubview(imageViewBin)
}else{
for subViews in self.subviews{
if subViews.isKindOfClass(UIImageView){
subViews.removeFromSuperview()
}
self.backgroundColor = deSelectedColorForTable
}
}
case .Ended:
if (!CGRectEqualToRect(CGRectIntersection(self.superview!.bounds, self.frame), self.frame)) //If its true then the view will be deleted.
{
self.removeFromSuperview()
}
default: break
}
}
The main problem is if the view is not rotated or transformed then all the "CGRectIntersection" inside the .Changed/.Ended case is working fine as expected but if the view is rotated or transformed then "CGRectIntersection" always becoming true even the view is inside the "mainView" and its removing from the mainview/superview.
Please help about my mistake.
Thanks in advance.
Frame of the view gets updated after applying transform. Following code ensures that it is inside the its superviews bounds.
if (CGRectContainsRect(self.superview!.bounds, self.frame))
{
//view is inside of the Superview
}
else
{
//view is outside of the Superview
}
I am trying to create a very simplified layout, something like Stripe's iOS app has which you can see here: https://stripe.com/img/dashboard_iphone/screencast.mp4
At around 0:06s the table view is swiped up and it moves up to the top of the window.
Are there any simple instructions in Swift that show this design pattern I see it everywhere but no idea how to create it
Add a UIView
Subclass that UIView with a custom class
In you custom UIView, you'll need a couple of variables and a few overrides. Make sure that user interaction is enabled on the UIView in your storyboard, then in your custom class add:
var startPosition: CGPoint?
var originalHeight: CGFloat?
After that, add the following:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch = touches.first
startPosition = touch?.locationInView(self)
originalHeight = self.frame.height
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch = touches.first
let endPosition = touch?.locationInView(self)
let difference = endPosition!.y - startPosition!.y
let newFrame = CGRectMake(self.frame.origin.x, self.frame.origin.y + difference, self.frame.width, self.frame.height - difference)
self.frame = newFrame
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
}
In UIScrollView.h:
// When the user taps the status bar, the scroll view beneath the touch which is closest to the status bar will be scrolled to top, but only if its `scrollsToTop` property is YES, its delegate does not return NO from `shouldScrollViewScrollToTop`, and it is not already at the top.
// On iPhone, we execute this gesture only if there's one on-screen scroll view with `scrollsToTop` == YES. If more than one is found, none will be scrolled.
var scrollsToTop: Bool // default is YES.
Initialize your scroll view delegate (unless you already have, probably in your viewDidLoad), and then set this to true.
Like this:
myScrollView.scrollsToTop = true
If you want to be sure your scroll view delegate allows this, check this method from UIScrollViewDelegate's protocol:
optional func scrollViewShouldScrollToTop(scrollView: UIScrollView) -> Bool // return a yes if you want to scroll to the top. if not defined, assumes YES
Please comment any concerns.
I have a floating UIView header below a UINavigationBar. I have inserted it as a subview at index 0, so I can animate it away and in according to contentOffset of a UITableView.
However, because it's beyond the bounds of the UINavigationBar I cannot receive touch events in this view after adding a UITapGestureRecognizer to it. All touch events go to the UITableView below it.
Any ideas if it's possible to have a subview outside of the bounds of the navigation bar and receive touch events for it?
I did this because adding it as a subview of a UITableView, I'd have to set the Y origin based on the contentOffset while scrolling and this also makes animations very difficult since the Y origin changes during scroll, so I can't know where to animate it to.
It's similar to the header in the Facebook app with "Status", "Photo" and "Check In" buttons.
Thanks
You need to create custom UINavigationBar and reimplement pointInside:withEvent: to extend touchable area of navigation bar (add your view area):
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
CGRect extendedRect = self.frame;
extendedRect.size.height += MENU_HEADER_HEIGHT - self.frame.origin.y;
BOOL pointInside = NO;
if (CGRectContainsPoint(extendedRect, point)) {
pointInside = YES;
}
return pointInside;
}
Adding to the above answer. Here is what I did.
class CustomNavigationBar: UINavigationBar {
override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
if(self.pointInside(point, withEvent: event)){
self.userInteractionEnabled = true
}else{
self.userInteractionEnabled = false
}
return super.hitTest(point, withEvent: event)
}
override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
var extendedRect:CGRect = self.frame
extendedRect.size.height += 100.0 - self.frame.origin.y
var pointInside:Bool = false
if(CGRectContainsPoint(extendedRect, point)){
pointInside = true
}
return pointInside
}
}
As far as I know you can't receive touch events outside the bounds of a view. So, I see a couple options:
First, you could go with the option of adding it as a subview to the UITableView, as you said, then adjusting its Y origin based on the tableview's contentOffset. When you want to animate it, instead of animating it to a specific y position you could do something like this:
[UIView animateWithDuration:0.4
animations:^{
_myHeaderView.frame = CGRectOffset(_myHeaderView.frame, 0.0, -_myHeaderView.frame.size.height);
}];
Another option I could see is adding the header view on top of everything, but with its origin sitting just below the UINavigationBar. Then when you animate it you could just move it to be below the UINavigationBar (or even actually pass it to the UINavigationBar) and animate after that so that it slides up underneath.
Swift 4 solution:
class TappableNavigationBar: UINavigationBar {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
isUserInteractionEnabled = self.point(inside: point, with: event)
return super.hitTest(point, with: event)
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
var extendedRect = frame
extendedRect.size.height += 300.0 - frame.origin.y
return extendedRect.contains(point)
}
}