I'm very new to Swift, and trying to create an app where Swiping between pages works normally but also involves a change in background color based on swipe distance.
Consequently, I want to "hijack" the swipe gesture, so I can add some behavior to it. The best I can find for how to do that is this question/answer.
Translating the code from the chosen answer into Swift 3, I added the following code to my RootViewController's viewDidLoad function:
for subview in (pageViewController?.view.subviews)!{
if let coercedView = subview as? UIScrollView {
coercedView.delegate = (self as! UIScrollViewDelegate)
}
}
My impression was that the above code would let me delegate functions (such as scrollViewDidScroll to the class in which I'm writing the above code, such that I can define that function, call super.scrollViewDidScroll, and then add any other functionality I want.
Unfortunately, the above code, which doesn't throw any compilation errors, does throw an error when I try to build the app:
Could not cast value of type 'My_App.RootViewController' (0x102cbf740) to 'UIScrollViewDelegate' (0x1052b2b00).
Moreover, when I try to write override func scrollViewDidScroll in my class, I get a compilation error telling me the function doesn't exist to override, which makes me think even if I got past the error, it wouldn't get called, and this isn't the right way to handle this issue.
I'm sorry this is such a noobish question, but I'm really quite confused about the basic architecture of how to solve this, and whether I understand the given answer correctly, and what's going wrong.
Am I interpreting delegate and that answer correctly?
Am I delegating to the correct object? (Is that the right terminology here?)
Is there a better way to handle this?
Am I coercing/casting improperly? Should I instead do:
view.delegate = (SomeHandMadeViewDelegateWhichDefinesScrollViewDidScroll as! UIScrollViewDelegate)
or something similar/different (another nested casting with let coercedSelf = self as? UIScrollViewDelegate or something?
Thanks!
If I understand your question correctly, you want to catch some scroll position and stuff right ? Then do
class RootViewController {
// Your stuff
for subview in pageViewController!.view.subviews {
if let coercedView = subview as? UIScrollView {
coercedView.delegate = self
}
}
extension RootViewController : UIScrollViewDelegate {
// Your scrollView stuff there
}
Related
This seems like it should have a simple answer, and probably does, but it's proving harder to find than I expected. As a specific example, let's say that I'm programming a chess game.
It seems like this is something I should be able to do just using CoreGraphics. It seems like using OpenGL or SpriteKit shouldn't be necessary.
I want to have a Board class that models the state of the board. Where should I declare my Board object? My impression is that it should be in the ViewController.
I want to have a view (actually a subview) that displays the current state of the board (by overloading drawRect). It should do this at the beginning, and should be updated when players make moves. How does the view access the data model to display the board state? Does giving the view a reference to the data violate MVC? If not, how would the reference be passed to the view? (I seem to just find lots of links about passing data between two ViewControllers.)
Should it instead be the ViewController "pushing" the data to the view whenever it needs to be drawn? My understanding, though, is that drawRect should not be called directly, and that instead setNeedsDisplay should be called, which will indirectly result in drawRect being called. This being the case, it's hard to see how the data would be passed.
Your code; your design decision. Nothing to comment on here.
You should have your model declaration in ViewController. True. That is how MVC works.
Having a reference of the data in a UIView DOES break MVC. Your view instance will not be independent anymore. Decoupling view and model is one of the main points of MVC and you are probably breaking it with this design.
What can you do about it?
Extending #Paulw11's comment, in your view controller you can declare a method that looks something like this :
func movePiece(somePiece : Piece, toSquare : Square) {
let pieceID = somePiece.id //I am just assuming the models structures
let pieceImageView = self.pieceImageViewFromID(id) //Assume that this method returns the reference of the image view. Assume that I am just working UIKit here.
let fromPoint : CGPoint = somePiece.origin
let toPoint : CGPoint = toSquare.coordinates
self.animateView(pieceImageView, fromPoint:fromPoint, toPoint:toPoint)
}
Note that in this design, the view is not holding any model references; the view controller will take care of setting its state and bring upon relevant animations.
If you are overriding drawRect:, then yes, for it be called, you should call setNeedsDisplay to update the changes. The view controller might call or you can add property observers to redraw itself based on a property change. One example for this could be:
class SillyView : UIView {
var drawPonies : Bool = false {
didSet {
if oldValue != drawPonies {
self.setNeedsDisplay()
}
}
}
override func drawRect(rect: CGRect) {
if drawPonies {
self.drawGoodLookingPony()
} else {
self.drawSomeOtherAnimal()
}
}
func drawGoodLookingPony() {
//Draw a good looking pony here
}
func drawSomeOtherAnimal() {
//Draw Something else
}
}
If your view controller decides to draw ponies all you have to do is, get the reference of the SillyView and set drawPonies to true.
self.sillyView.drawPonies = true
You are not passing your data model here, but important pieces of configuration information that will help the view redraw itself.
I've had this working in other variations but something seems to elude me in the change from objective-c to swift as well as moving some of the setup into it's own class.
So i have:
class ViewController: UIViewController, interfaceDelegate, scrollChangeDelegate{
let scrollControl = scrollMethods()
let userinterface = interface()
override func viewDidLoad(){
super.viewDidLoad()
loadMenu("Start")
}
func loadMenu(menuName: String) {
userinterface.delegate = self
userinterface.scrollDelegate = self
userinterface.removeFromSuperview() //no impact
scrollControl.removeFromSuperview() //no impact
userinterface.configureView(menuName)
view.addSubview(scrollControl)
scrollControl.addSubview(userinterface)
}
}
This sets everything up correctly but the problem occurs when I change loadMenu() at runtime. So if the user calls loadMenu("AnotherMenu") it won't change the UIView. It will call the right functions but it won't update the view. Although if I call loadMenu("AnotherMenu") at the start, the correct menu will display. Or if I call loadMenu("Start") and then loadMenu("AnotherMenu") then the menu displayed will be "AnotherMenu". As in:
override func viewDidLoad(){
super.viewDidLoad()
loadMenu("Start")
loadMenu("AnotherMenu")
}
When I list all the subviews each time loadMenu() is called, they look correct. Even during runtime. But the display is not updated. So something isn't getting the word. I've tried disabling Auto Layout after searching for similar issues but didn't see a difference.
Try adding setNeedsDisplay() to loadMenu
Eg
func loadMenu(menuName: String) {
userinterface.delegate = self
userinterface.scrollDelegate = self
userinterface.removeFromSuperview() //no impact
scrollControl.removeFromSuperview() //no impact
userinterface.configureView(menuName)
view.addSubview(scrollControl)
scrollControl.addSubview(userinterface)
view.setNeedsDisplay()
}
setNeedsDisplay() forces the view to reload the user interface.
I didn't want to post the whole UIView class as it is long and I thought unrelated. But Dan was right that he would need to know what was going on in those to figure out the answer. So I created a dummy UIView class to stand in and intended to update the question with that. I then just put a button on the ViewController's UIView. That button was able to act on the view created by the dummy. So the problem was in the other class. Yet it was calling the methods of the ViewController and seemingly worked otherwise. So then the issue must be that its acting on an instanced version? The way the uiview class worked, it uses performSelector(). But in making these methods into their own class, I had just lazily wrote
(ViewController() as NSObjectProtocol).performSelector(selector)
when it should have been
(delegate as! NSObjectProtocol).performSelector(selector)
so that was annoying and I wasted the better part of a day on that. But thanks again for the help.
Having just spent a day beating my head against the keyboard, I thought I'd share my diagnosis and solution.
Situation: You add a custom View of custom class CardView to an enclosing view myCards in your app and want each card to respond to a tap gesture (for example to indicate you want to discard the card). Typical code you start with:
In your ViewController:
class MyVC : UIViewController, UIGestureRecognizerDelegate {
...
func discardedCard(sender: UITapGestureRecognizer) {
let cv : CardView = sender.view! as! CardView
...
}
In your myCards construction:
cv = CardView(card: card)
myCards.addSubview(cv)
cv.userInteractionEnabled = true
...
let cvTap = UITapGestureRecognizer(target: self, action: Selector("discardedCard:"))
cvTap.delegate = self
cv.addGestureRecognizer(cvTap)
I found the arguments here very confusing and the documentation not at all helpful. It isn't clear that the target: argument refers to the class that implements discardedCard(sender: UITapGestureRecognizer) . If you're constructing the recognizer and cards in your ViewController that's going to be self. If you want to move the discardedCard into your custom View class (for example), then replace self with CardView in my example, including on the delegate line.
Testing the above code, I found that the discardedCard function was never called. What was going on?
So a day later, here's what I had to fix. I hope this checklist is useful to somebody else. I'm new to iOS (coming from Android), so it may be obvious to you veterans:
Make sure the touched view (cv in the example) has userInteractionEnabled=true . Note that if you use your own constructor it will be set false by default
Make sure all enclosing views also have userInteractionEnabled=true
Others have posted that the order of the delegate statement and addGestureRecognizer statement made a difference; I didn't find that using Xcode 7.2 and iOS 9.2
Most important: Make sure your touched view is fully within the bounds of the enclosing views. In my example, I was building a myCards container that didn't have the width set correctly and was cutting off the right-most cards (and since clipping is disabled by default, this wasn't visually obvious until I looked in the debugger at the View hierarchy)
I am building an app for keeping track of reading assignments for a university course. Each ReadingAssignment has included a Bool value that indicates if the reader has finished reading the assignment. The ReadingAssignments are collected into WeeklyAssignment arrays. I want to have the user be able to touch a label and have a checkmark appear and show the assignment as completed. I would like this touch to also update the .checked property to true so I can persist the data.
So, I am trying to have the gestureRecognizer call the labelTicked() method. This works and prints to the console. However, when I try to pass in the assignment parameter, it compiles, but crashes on the touch with an "unrecognized selector" error. I have read every topic i can find here, and haven't found the solution. They all say ":" signifies a Selector with parameters, but still no go.
Can you see what I am doing wrong?
func configureCheckmark(cell: UITableViewCell, withWeeklyAssignment assignment: WeeklyAssignment) {
let checkLabel = cell.viewWithTag(1002) as! UILabel
checkLabel.userInteractionEnabled = true
let gestureRecognizer = UITapGestureRecognizer(target: self, action: Selector("labelTicked:assignment"))
checkLabel.addGestureRecognizer(gestureRecognizer)
}
#objc func labelTicked(assignment: WeeklyAssignment) {
assignment.toggleCheckmark()
if assignment.checked {
label.text = "✔︎"
} else {
label.text = ""
}
}
I would also love to pass in the UILabel checkLabel so I can update it in the labelTicked() method.
Thanks for your help.
There are two distinct problems here:
The syntax for the selector is wrong; the : doesn't mark the beginning of a parameters part, it merely marks that this function takes parameters at all. So the tap recognizer should be initialized as UITapGestureRecognizer(target: self, action: "labelTicked:"). Easy fix. That is the good news.
The bad news: even with perfect syntax it will not work the way you set it up here. Your tap recognizer cannot pass a WeeklyAssignment object as a parameter. In fact, it cannot pass any custom parameter at all. At least, not like that.
However, you can pass it in the sender (which is usually the view the gesture recognizer is attached to). You can grab it by changing your method to
func labelTicked(sender: AnyObject) {
(note that AnyObject may be declared as a more specific type if you know exactly what to expect.)
Going through the sender, you could now theoretically infer which label it is that has been tapped, which data entity that labels corresponds to, and which state the checked property of that entity is in. I think this would become very convoluted very quickly.
Seemingly straightforward things becoming convoluted is usually a good sign that we should take a step back and look for a better solution.
I'd suggest dropping the whole GestureRecognizer approach at this point, and instead exploit the fact that each cell in a table view already comes with its own "tap recongizing" functionality out of the box: the didSelectRowAtIndexPath: method of the table view's delegate. There, you can easily use the supplied NSIndexPath to retrieve the corresponding model entity from the data source to read and modify its parameters as you see fit. Via cellForRowAtIndexPath: you can then get a reference to the correct cell and change its contents accordingly.
Update for Swift 3:
As the Swift language evolves and using String-based selectors (e.g. "labelTicked:") is now flagged as deprecated, I think it's appropriate to provide a small update to the answer.
Using more modern syntax, you could declare your function like this:
#objc func labelTicked(withSender sender: AnyObject) {
and initialize your gesture recognizer like this, using #selector:
UITapGestureRecognizer(target: self, action: #selector(labelTicked(withSender:)))
The correct selector for that function is labelTicked:. Furthermore, you can use a string directly as a selector. Thus:
let gestureRecognizer = UITapGestureRecognizer(target: self, action: "labelTicked:")
But you can't arrange to pass an arbitrary object along to the method when the recognizer fires. A better way to do this is to create a subclass of UITableViewCell. Let's call it AssignmentCell. Give the subclass an assignment property, and move the labelTicked: method to AssignmentCell.
If you've designed the cell in your storyboard, you can add a tap recognizer to the label right in the storyboard, and wire the recognizer to the cell's labelTicked: method in the storyboard.
In your table view data source's tableView(_:cellForRowAtIndexPath:), after you've dequeued the cell, set its assignment property to the assignment for that row.
I have a UIViewController (let's call it "EditViewController") which has a Container View on it (call it "ContainerView") where I switch in and out various subviews (call the one I'm most concerned with "EditDetailsView").
From the EditDetailsView I need to change the title in the navigation bar of the EditViewController. I can't seem to be able to figure out how to reference it.
From inside EditViewController I can simply make a statement like:
self.title = #"Some new title";
and it changes just fine.
But from the EditDetailsView view that is currently the subview of ContainerView nothing seems to work:
self.title = ... is obviously wrong.
super.title = ... doesn't work and seems wrong anyway.
super.super.title = ... errors out as super is not a property found on UIViewController.
Can someone please tell me how to reference the title? I'm kinda lost.
Thanks!
While digging through the parentViewController chain is possible, it is error prone and unrecommended. It is considered a bad design. Imagine you set up your view controller hierarchy in some manner, but after a few months change it a bit and now there is one level deeper. Or, you would like to use the same view controller in several different scenarios. A much better design would be to pass the new title to the container view controller using delegation. Create a delegate protocol, with a method for setting the title.
- (void)childViewController:(ChildViewController*)cvc didChangeToTitle:(NSString*)title;
I know this is an old thread, but in case someone else needs it: to avoid boilerplate code with delegation, and avoid digging into the parentViewController, I did it the other way around.
I've referenced the child view controller from the parent and got their title. So no matter which child you show, you will always get the right title.
This is in Swift 3.
So, basically, this is your parent:
class EditViewController: UIViewController {
override func viewDidLoad() {
if let child = self.childViewControllers.first {
self.title = child.title
}
}
}
And this is your child:
class ContainerView: UIViewController {
override func viewDidLoad() {
self.title = "Sbrubbles"
}
}
Another good way to avoid excess code with delegation is to use RxSwift, if you are familiar to Reactive programming.