Recognizing subview's class - ios

I decided to animate my objects manually and therefore made an extension for UIView class:
public extension UIView{
func slideOut(){
UIView.animateWithDuration(0.5, animations: { self.frame.origin.x = -self.frame.width }, completion: finishedDisposing)
}
func finishedDisposing(successfully: Bool){
if !successfully{
((UIApplication.sharedApplication().delegate as! AppDelegate).window!.rootViewController as! VC).showSystemMessage("Failed to dispose one or more subviews from superview", ofType: .NOTICE)
}
responder.viewDisposed()
}
}
Which works nice and I have no problems about it, BUT I have a method in VC (Custom UIViewController) viewDisposed() which is called whenever a view slides out of sight and it has such an implementation:
func viewDisposed() {
disposed++
print("Updated disposed: \(disposed) / \(self.view.subviews.count)")
if disposed == self.view.subviews.count - 1{
delegate.vcFinishedDisposing()
}
}
It shows that self.view.subviews contains all my custom views + 3 more (UIView, _UILayoutGuide x 2). They do extend UIView although do not callresponder.viewDisposed method. My decision was to figure out how to get classes of each subview and Mirror(reflecting: subView).subjectType if I print it does it wonderfully. Is there any way to actually compare this to anything or, better, get String representation? Basically, I want you to help me create a method which would create a stack of subviews which are not of type UIView (only subClasses) nor _UILayoutGuide. Thank you!

You'd probably be better off directly creating an array of just the subviews you care about, instead of starting with all subviews and trying to filter out the ones you don't care about. Those layout guides weren't always there—they were added in iOS 7. Who knows what else Apple will add in the future?
Anyway:
let mySubviews = view.subviews.filter {
!["UIView", "_UILayoutGuide"].contains(NSStringFromClass($0.dynamicType))
}

Related

How can I determine the index path for the currently focused UITableViewCell using Voice Over?

I have a dynamic UITableView. For each cell, I add a UIAccessibilityCustomAction. When the action fires, I need to know the index path so I can respond accordingly and update my model.
In tableView(_:cellForRowAt:) I add my UIAccessibilityCustomAction like this...
cell.accessibilityCustomActions = [
UIAccessibilityCustomAction(
name: "Really Bad Name",
target: self,
selector: #selector(doSomething)
)
]
I have tried to use UIAccessibility.focusedElement to no avail...
#objc private func doSomething() {
let focusedCell = UIAccessibility.focusedElement(using: UIAccessibility.AssistiveTechnologyIdentifier.notificationVoiceOver) as! UITableViewCell
// Do something with the cell, like find the indexPath.
}
The problem is that casting to a cell fails. The debugger says that the return value type is actually a UITableTextAccessibilityElement, which I could find no information on.
When the action fires, I need to know the index path so I can respond accordingly and update my model.
The best way to reach your goal is to use the UIAccessibilityFocus informal protocol methods by overriding them in your object directly (the table view cell class in your case): you'll be able to catch the needed index path when a custom action is fired.
I suggest to take a look at this answer dealing with catching accessibility focus changed that contains a detailed solution with code snippets if need be.😉
Example snippet...
class SomeCell: UITableViewCell
override open func accessibilityElementDidBecomeFocused() {
// Notify view controller however you want (delegation, closure, etc.)
}
}
I ended up having to solve this myself to bodge an Apple bug. You've likely solved this problem, but this is an option similar to your first suggestion.
func accessibilityCurrentlySelectedIndexPath() -> IndexPath? {
let focusedElement:Any
if let voiceOverObject = UIAccessibility.focusedElement(using: UIAccessibility.AssistiveTechnologyIdentifier.notificationVoiceOver) {
focusedElement = voiceOverObject
} else if let switchControlObject = UIAccessibility.focusedElement(using: UIAccessibility.AssistiveTechnologyIdentifier.notificationSwitchControl) {
focusedElement = switchControlObject
} else {
return nil
}
let accessibilityScreenFrame:CGRect
if let view = focusedElement as? UIView {
accessibilityScreenFrame = view.accessibilityFrame
} else if let accessibilityElement = focusedElement as? UIAccessibilityElement {
accessibilityScreenFrame = accessibilityElement.accessibilityFrame
} else {
return nil
}
let tableViewPoint = UIApplication.shared.keyWindow!.convert(accessibilityScreenFrame.origin, to: tableView)
return tableView.indexPathForRow(at: tableViewPoint)
}
What we're essentially doing here is getting the focused rect (in screen coordinates) and then translating it back to the table view's coordinate space. We can then ask the table view for the indexpath which contains that point. Simple and sweet, though if you're using multi-window you may need to swap UIApplication.shared.keyWindow! with something more appropriate. Note that we deal with the issue you faced where the element was a UITableTextAccessibilityElement when we handle UIAccessibilityElement since UITableTextAccessibilityElement is a private, internal Apple class.

Is there a better way to do this scrolling animation in Swift?

I'm trying to add a behavior to part of my app's UI whereby the user can swipe left and this will result in a set of UILabels scrolling in sync together off-screen to the left, but then immediately scrolling back in again but from the right, but with new information contained in them.
The effect is meant to give the impression that you're moving from one "set" of info to the next... like, say, choosing a car before starting a race game... but in reality it is the same views being re-used... scrolling offscreen to have their label.text info updated... then scrolling back in again.
I have the swiping all taken care of. The issue I'm having is that my (working) solution:
UIView.animate(withDuration: 0.2, animations: {
// move label off to the left
self.titleLabel.center.x -= self.view.bounds.width
}, completion: {
$0 ; print("I'm halfway done!")
// teleport view to a location off to the right
self.titleLabel.center.x += 2*(self.view.bounds.width)
// reset label's data
self.titleLabel.text = NEW_INFO
// slide label back on screen from the right
UIView.animate(withDuration: 0.2, animations: {
self.titleLabel.center.x -= self.view.bounds.width
}, completion: nil)
})
Feels trashy like wearing someone else's underwear.
The only reason that $0 is there is to make XCode stop saying:
"Cannot convert value of type '() -> ()' to expected argument type '((Bool) -> Void)?'"
And I'm sure the fact that I'm doing the second part of the animation in a completion block will cause headaches down the road.
Is there a smarter way?
PS - I would prefer not to use any pre-made classes like "ScrollView" or anything like that... these views are all individually interactive and respond to other callbacks etc.
Thanks!
A better approach to something like this would be to use a UIPageViewController.
It will take a bit of learning and set up but is much easier than trying to roll it yourself.
The approach to take with a UIPageViewController is something like this...
Create a data model... to use your analogy...
struct Car {
let image: UIImage
let name: String
}
Then create a UIViewController subclass that will display it.
class CarViewController: UIViewController {
var car: Car? {
didSet {
displayCar()
}
}
func displayCar() {
label.text = car?.name
imageView.image = car?.image
}
}
Then you create a UIPageViewController. Inside this you have an array of cars. And in the function func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? you can then create your CarViewController and pass in the correct Car from the array.
This will then do all your scrolling and displaying and everything is still interactive.
For more information about how this works you can look at tutorials like this one from Ray Wenderlich.
You can also use this to display a part of a page (rather than scrolling the entire screen.

Tracking swipe movement in Swift

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
}

Abstracting gesture recognizer to two functions in Swift?

Imagine an iOS screen where you can move your finger up/down to do something (imagine say, "scale"),
Or,
as a separate function you can move your finger left/right to do something (imagine say, "change color" or "rotate").
They are
separate
functions, you can only do one at at time.
So, if it "begins as" a horizontal version, it is remains only a horizontal version. Conversely if it "begins as" a vertical version, it remains only a vertical version.
It is a little bit tricky to do this, I present exactly how to do it below...the fundamental pattern is:
if (r.state == .Began || panway == .WasZeros )
{
prev = tr
if (tr.x==0 && tr.y==0)
{
panway = .WasZeros
return
}
if (abs(tr.x)>abs(tr.y)) ... set panway
}
This works very well and here's exactly how to do it in Swift.
In storyboard take a UIPanGestureRecognizer, drag it to the view in question. Connect the delegate to the view controller and set the outlet to this call ultraPan:
enum Panway
{
case Vertical
case Horizontal
case WasZeros
}
var panway:Panway = .Vertical
var prev:CGPoint!
#IBAction func ultraPan(r:UIPanGestureRecognizer!)
{
let tr = r.translationInView(r.view)
if (r.state == .Began || panway == .WasZeros )
{
prev = tr
if (tr.x==0 && tr.y==0)
{
panway = .WasZeros
return
}
if (abs(tr.x)>abs(tr.y))
{
panway = .Horizontal
}
else
{
panway = .Vertical
}
}
if (panway == .Horizontal) // your left-right function
{
var h = tr.x - prev.x
let sensitivity:CGFloat = 50.0
h = h / sensitivity
// adjust your left-right function, example
someProperty = someProperty + h
}
if (panway == .Vertical) // bigger/smaller
{
var v = tr.y - prev.y
let sensitivity:CGFloat = 2200.0
v = v / sensitivity
// adjust your up-down function, example
someOtherProperty = someOtherProperty + v
}
prev = tr
}
That's fine.
But it would surely be better to make a new subclass (or something) of UIPanGestureRecognizer, so that there are two new concepts......
UIHorizontalPanGestureRecognizer
UIVerticalPanGestureRecognizer
Those would be basically one-dimensional panners.
I have absolutely no clue whether you would ... subclass the delegates? or the class? (what class?), or perhaps some sort of extension ... indeed, I basically am completely clueless on this :)
The goal is in one's code, you can have something like this ...
#IBAction func horizontalPanDelta( ..? )
{
someProperty = someProperty + delta
}
#IBAction func verticalPanDelta( ..? )
{
otherProperty = otherProperty + delta
}
How to inherit/extend UIPanGestureRecognizer in this way??
But it would surely be better to make a new subclass (or something) of UIPanGestureRecognizer, so that there are two new concepts......
UIHorizontalPanGestureRecognizer
UIVerticalPanGestureRecognizer
Those would be basically one-dimensional panners
Correct. That's exactly how to do it, and is the normal approach. Indeed, that is exactly what gesture recognizers are for: each g.r. recognizes only its own gesture, and when it does, it causes the competing gesture recognizers to back off. That is the whole point of gesture recognizers! Otherwise, we'd still be back in the pre-g.r. days of pure touchesBegan and so forth (oh, the horror).
My online book discusses, in fact, the very example you are giving here:
http://www.apeth.com/iOSBook/ch18.html#_subclassing_gesture_recognizers
And here is an actual downloadable example that implements it in Swift:
https://github.com/mattneub/Programming-iOS-Book-Examples/blob/master/bk2ch05p203gestureRecognizers/ch18p541gestureRecognizers/HorizVertPanGestureRecognizers.swift
Observe the strategy used here. We make UIPanGestureRecognizer subclasses. We override touchesBegan and touchesMoved: the moment recognition starts, we fail as soon as it appears that the next touch is along the wrong axis. (You should watch Apple's video on this topic; as they say, when you subclass a gesture recognizer, you should "fail early, fail often".) We also override translationInView so that only movement directly along the axis is possible. The result (as you can see if you download the project itself) is a view that can be dragged either horizontally or vertically but in no other manner.
#Joe, this is something I scrambled together quickly for the sake of this question. For ease of making it, I simply gave it a callback rather than implementing a target-action system. Feel free to change it.
enum PanDirection {
case Horizontal
case Vertical
}
class OneDimensionalPan: NSObject {
var handler: (CGFloat -> ())?
var direction: PanDirection
#IBOutlet weak var panRecognizer: UIPanGestureRecognizer! {
didSet {
panRecognizer.addTarget(self, action: #selector(panned))
}
}
#IBOutlet weak var panView: UIView!
override init() {
direction = .Horizontal
super.init()
}
#objc private func panned(recognizer: UIPanGestureRecognizer) {
let translation = panRecognizer.translationInView(panView)
let delta: CGFloat
switch direction {
case .Horizontal:
delta = translation.x
break
case .Vertical:
delta = translation.y
break
}
handler?(delta)
}
}
In storyboard, drag a UIPanGestureRecognizer onto your view controller, then drag an Object onto the top bar of the view controller, set its class, and link its IBOutlets. After that you should be good to go. In your view controller code you can set its callback and pan direction.
UPDATE FOR EXPLANATION >
I want to clarify why I made the class a subclass of NSObject: the driving idea behind the class is to keep any unnecessary code out of the UIViewController. By subclassing NSObject, I am able to then drag an Object onto the view controller's top bar inside storyboard and set its class to OneDimensionalPan. From there, I am able to connect #IBOutlets to it. I could have made it a base class, but then it would have had to be instantiated programmatically. This is far cleaner. The only code inside the view controller is for accessing the object itself (through an #IBOutlet), setting its direction, and setting its callback.

UIView doesn't change at runtime

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.

Resources