hello there i am trying to figure out what the issue is why i cant get a cross on the game board when a tap is made. the TTT class is the one below the first code and it is called in the main code. i will appreciate if some can help me on this
#IBOutlet var fields: [TTTImageView]!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
func fieldTapped (recognizer:UITapGestureRecognizer) {
let tappedField = recognizer.view as! TTTImageView
tappedField.setPlayer(_player: "x.png")
}
func setupField () {
for index in 0 ... fields.count - 1 {
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(fieldTapped))
gestureRecognizer.numberOfTapsRequired = 1
fields[index].addGestureRecognizer(gestureRecognizer)
}
}
import UIKit
class TTTImageView: UIImageView {
var player:String?
var activated:Bool! = false
func setPlayer (_player:String) {
self.player = _player
if activated == false {
if _player == "x.png" {
self.image = UIImage (named: "x.png")
}else{
self.image = UIImage (named: "o.png")
}
activated = true
}
}
}
Make sure when you set fields[index].addGestureRecognizer(gestureRecognizer) to set isUserInteractionEnabled to true.
fields[index].isUserInteractionEnabled = true
Whenever you want to detect user activity (e.g. UITapGestureRecognizer on a UIImageView, you need to set this to true
EDIT:
I also wanted to mention something I noticed after looking at your code again: the method you are using for selectors has been deprecated. If you are using Swift 3 (which I highly recommend you do), you shouldn't call selectors like this: "functionName:" anymore. Now we use selectors like so: #selector(functionName(_:)). You should start updating your code to the most current syntax sooner rather than later.
Related
I am trying to make an app where the user could drag a finger on top of multiple buttons and get some actions for each button.
There was a similar question from a while back but when I tried to use CGRectContainsPoint(button.frame, point) it said it was replaced with button.frame.contains(point) but this didn’t seem to work for me. Here is a link to the Photo
What I have done so far:
var buttonArray:NSMutableArray!
override func viewDidLoad()
{
super.viewDidLoad()
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(panGestureMethod(_:)))
a1.addGestureRecognizer(panGesture)
a2.addGestureRecognizer(panGesture)
}
#objc func panGestureMethod(_ gesture: UIPanGestureRecognizer) {
if gesture.state == UIGestureRecognizer.State.began {
buttonArray = NSMutableArray()
}
let pointInView = gesture.location(in: gesture.view)
if !buttonArray.contains(a1!) && a1.frame.contains(pointInView) {
buttonArray.add(a1!)
a1Tapped(a1)
}
else if !buttonArray.contains(a2!) && a2.frame.contains(pointInView) {
buttonArray.add(a2!)
a2Tapped(a2)
}
The code did run fine but when I tried to activate the drag nothing happened. Any tips?
You want behavior similar to the system keyboard I assume? CGRectContainsPoint is not deprecated: See the docs. In Swift it's written like frame.contains().
When dealing with rects and points you have to make sure both are translated to the same coordinate system first. To do so you can use the convert to/from methods on UIView: See (the docs).
In your case smth. like the following should work (first translate button frames, then check if the point is inside):
func touchedButtonForGestureRecognizer(_ gesture: UIPanGestureRecognizer) -> UIView? {
let pointInView = gesture.location(in: gesture.view)
for (button in buttonArray) {
let rect = gesture.view.convert(button.frame from:button.superview)
if (rect.contains(pointInView)) {
return button
}
}
return nil
}
I am having a no-show menuController and I have checked all of the suggestions in previous questions. It turns out the imageView I have implemented a UILongPressGestureRecognizer on, to show the menu, is returning False on calling .becomeFirstResponder just before setting up the menu controller.
I am coding in swift 4 and can't figure out how to make the imageView return True to calling .becomeFirstResponder. Help!
/*********************************************************/
override func viewDidLoad() {
super.viewDidLoad()
// long tap to show menu that enables deletion of the image.
imageView_1.isUserInteractionEnabled = true
let longPressRecogniser = UILongPressGestureRecognizer(target: self, action: #selector(longPressOnImage(_:)))
//longPressRecogniser.numberOfTapsRequired = 1
//longPressRecogniser.numberOfTouchesRequired = 1
longPressRecogniser.minimumPressDuration = 0.5
imageView_1.addGestureRecognizer(longPressRecogniser)
imageView_1.image = placeHolderImage_1
imageView_2.image = placeHolderImage_2
}
/*********************************************************/
#IBAction func longPressOnImage(_ gestureRecognizer: UILongPressGestureRecognizer) {
print(#function)
if gestureRecognizer.state == .began {
//print("gestureRecognizer.state == .began")
self.tappedView = gestureRecognizer.view!
if tappedView.canResignFirstResponder {
print("can resign first responder")
}
if tappedView.becomeFirstResponder() {
print("returned TRUE to becomeFirstResponder")
} else {
print("returned FALSE to becomeFirstResponder")
}
// Configure the shared menu controller
let menuController = UIMenuController.shared
// Configure the menu item to display
// Create a "delete" menu item
let deleteImage = UIMenuItem(title: "Delete", action: #selector(deleteImage_1))
menuController.menuItems = [deleteImage]
// Set the location of the menu in the view.
let location = gestureRecognizer.location(in: tappedView)
print("location = ", location)
let menuLocation = CGRect(x: location.x, y: location.y, width: 2, height: 2)
menuController.setTargetRect(menuLocation, in: tappedView)
//update the menu settings to force it to display my custom items
menuController.update()
// Show the menu.
menuController.setMenuVisible(true, animated: true)
print("menu should be visible now")
}
}
/*********************************************************/
#objc func deleteImage_1() {
print(#function)
}
My caveman debugging print statements output:
longPressOnImage
can resign first responder
returned FALSE to becomeFirstResponder
location = (207.0, 82.0)
menu should be visible now
Create a custom imageView class and override "canBecomeFirstResponder" property like this:
class ResponsiveImage : UIImageView{
override var canBecomeFirstResponder: Bool{
return true
}
}
Use this ResponsiveImage type and your code will work :)
Thank you to adri. Your answer is the solution to my problem.
I had read in other posts to similar questions about overriding var canBecomeFirstResponder but either overlooked it or it wasn't made explicit that a custom UIImageView class needs to be created.
Just to make it clear to newbies like me, the class of the imageView in storyBoard and its #IBOutlet in its viewController must typed as ResponsiveImage. If only one of these is changed a type casting error is reported.
Many thanks for ending my hours of frustration! :-)
I am doing an app that does background job that can take some time
I want to show a loader in that time
I want a black screen with a simple loader in the front of it
and show it \ hide it,
when I do actions in the background
I want to do a simple half black square with loader circle
that also blocks presses to the screen
Like in this picture:
How can I achieve that and that ?
First create one UIView which you will put in front of your LogIn view. Then add UIActivityIndicatorView to the created UIView.
let loadingIndicatorView = UIView()
let activityIndicatorView = UIActivityIndicatorView(activityIndicatorStyle: .gray)
Now the loadingIndicatorView should have same frame size as your LogIN view. For color you can set your own color with alpha as you want to show LogIn content too. Initially keep it hidden and whenever you want to show it unhide it.
loadingIndicatorView.frame = view.frame
loadingIndicatorView.backgroundColor = .gray
loadingIndicatorView.isHidden = true
Now setup activityIndicatorView, it should be shown at centre,
activityIndicatorView.center = CGPoint(
x: UIScreen.main.bounds.size.width / 2,
y: UIScreen.main.bounds.size.height / 2
)
You can set some color to the indicator,
activityIndicatorView.color = .white
activityIndicatorView.hidesWhenStopped = true
Now add this activityIndicatorView to loadingIndicatorView and loadingIndicatorView to LogIn View.
loadingIndicatorView.addSubview(activityIndicatorView)
view.addSubview(loadingIndicatorView)
Lastly for showing do,
loadingIndicator.startAnimating()
loadingIndicatorView.isHidden = false
And for hiding,
loadingIndicator.stopAnimating()
loadingIndicatorView.isHidden = true
Updated Answer
Since the OP wanted an example code. Hence the updated answer. Hope everyone gets to learn something or the other out of it.
To start with, I created a subclass of UIView and named it PSOverlaySpinner and it looks something like below:
import UIKit
class PSOverlaySpinner: UIView {
//MARK: - Variables
private var isSpinning: Bool = false
private lazy var spinner : UIActivityIndicatorView = {
var spinner = UIActivityIndicatorView(style: UIActivityIndicatorView.Style.white)
spinner.translatesAutoresizingMaskIntoConstraints = false
spinner.hidesWhenStopped = true
return spinner
}()
// MARK: - View Lifecycle Functions
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
init() {
super.init(frame: CGRect.zero)
self.translatesAutoresizingMaskIntoConstraints = false
self.backgroundColor = UIColor.init(white: 0.0, alpha: 0.8)
self.isSpinning = false
self.isHidden = true
createSubviews()
}
deinit {
self.removeFromSuperview()
}
func createSubviews() -> Void {
self.addSubview(spinner)
setupAutoLayout()
}
// MARK: - Private Methods
private func setupAutoLayout() {
if #available(iOS 11.0, *) {
spinner.safeAreaLayoutGuide.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor).isActive = true
spinner.safeAreaLayoutGuide.centerYAnchor.constraint(equalTo: safeAreaLayoutGuide.centerYAnchor).isActive = true
} else {
// Fallback on earlier versions
spinner.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
spinner.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
}
}
// MARK: - Public Methods
public func show() -> Void {
DispatchQueue.main.async {
if !self.spinner.isAnimating {
self.spinner.startAnimating()
}
self.isHidden = false
}
isSpinning = true
}
public func hide() -> Void {
DispatchQueue.main.async {
if self.spinner.isAnimating {
self.spinner.stopAnimating()
}
self.isHidden = true
}
isSpinning = false
}
}
Now move onto the ViewController that you want to add this overlay view to. Since I create my views programmatically, I will show how to do it the same way, but you can easily do it via storyboard or xibs.
Step 1 : Initialize
public lazy var spinnerView : PSOverlaySpinner = {
let loadingView : PSOverlaySpinner = PSOverlaySpinner()
return loadingView
}()
Step 2 : Add as a subview
self.view.addSubview(spinnerView)
Step 3 : Set constraints
spinnerView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
spinnerView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
spinnerView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
spinnerView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
Step 4 : To show PSOverlaySpinner
spinnerView.show()
Step 5 : To hide PSOverlaySpinner
spinnerView.hide()
That is it!!
If you want you can go ahead and modify the PSOverlaySpinner as per your needs. For example, you might want to add a UILabel below the spinner indicating him of the type of action taking place and so on.
Before
After
Old Answer
If you wish to do it manually then create a UIView with the its frame matching self.view.bounds, with 0.5-0.7 alpha and black background color. Add UIActivityIndicator as its subview constrained to its center. For a spinner specific to the image you will have to use the open sourced spinners made available. A couple of them can be found here. Once done add this view as the topmost subview in self.view.
You need to import this library SVProgressHUD and then set few properties like as follows:
SVProgressHUD.setDefaultStyle(SVProgressHUDStyle.dark)
SVProgressHUD.setBackgroundColor(.clear)
SVProgressHUD.setForegroundColor(.white)
SVProgressHUD.setDefaultMaskType(.black)
SVProgressHUD.show()
//SVProgressHUD.show(withStatus: "Loading something, Loading something,Loading something ...")
This will produce same UI output as needed by you in OP. You can find a running sample at my repository (TestPreLoader)
Consider this trivial sync animation using CADisplayLink,
var link:CADisplayLink?
var startTime:Double = 0.0
let animTime:Double = 0.2
let animMaxVal:CGFloat = 0.4
private func yourAnim()
{
if ( link != nil )
{
link!.paused = true
//A:
link!.removeFromRunLoop(
NSRunLoop.mainRunLoop(), forMode:NSDefaultRunLoopMode)
link = nil
}
link = CADisplayLink(target: self, selector: #selector(doorStep) )
startTime = CACurrentMediaTime()
link!.addToRunLoop(
NSRunLoop.currentRunLoop(), forMode:NSDefaultRunLoopMode)
}
func doorStep()
{
let elapsed = CACurrentMediaTime() - startTime
var ping = elapsed
if (elapsed > (animTime / 2.0)) {ping = animTime - elapsed}
let frac = ping / (animTime / 2.0)
yourAnimFunction(CGFloat(frac) * animMaxVal)
if (elapsed > animTime)
{
//B:
link!.paused = true
link!.removeFromRunLoop(
NSRunLoop.mainRunLoop(), forMode:NSDefaultRunLoopMode)
link = nil
yourAnimFunction(0.0)
}
}
func killAnimation()
{
// for example if the cell disappears or is reused
//C:
????!!!!
}
There seems to be various problems.
At (A:), even though link is not null, it may not be possible to remove it from a run loop. (For example, someone may have initialized it with link = link:CADisplayLink() - try it for a crash.)
Secondly at (B:) it seems to be a mess ... surely there's a better (and more Swift) way, and what if it's nil even though the time just expired?
Finally in (C:) if you want to break the anim ... I got depressed and have no clue what is best.
And really the code at A: and B: should be the same call right, kind of a clean-up call.
Here’s a simple example showing how I’d go about implementing a CADisplayLink (in Swift 5):
class C { /// your view class or whatever
private var displayLink: CADisplayLink?
private var startTime = 0.0
private let animationLength = 5.0
func startDisplayLink() {
stopDisplayLink() /// make sure to stop a previous running display link
startTime = CACurrentMediaTime() // reset start time
/// create displayLink and add it to the run-loop
let displayLink = CADisplayLink(target: self, selector: #selector(displayLinkDidFire))
displayLink.add(to: .main, forMode: .common)
self.displayLink = displayLink
}
#objc func displayLinkDidFire(_ displayLink: CADisplayLink) {
var elapsedTime = CACurrentMediaTime() - startTime
if elapsedTime > animationLength {
stopDisplayLink()
elapsedTime = animationLength /// clamp the elapsed time to the animation length
}
/// do your animation logic here
}
/// invalidate display link if it's non-nil, then set to nil
func stopDisplayLink() {
displayLink?.invalidate()
displayLink = nil
}
}
Points to note:
We’re using nil here to represent the state in which the display link isn’t running – as there’s no easy way of getting this information from an invalidated display link.
Instead of using removeFromRunLoop(), we’re using invalidate(), which will not crash if the display link hasn’t already been added to a run-loop. However this situation should never arise in the first place – as we’re always immediately adding the display link to the run-loop after creating it.
We’ve made the displayLink private in order to prevent outside classes from putting it in an unexpected state (e.g invalidating it but not setting it to nil).
We have a single stopDisplayLink() method that both invalidates the display link (if it is non-nil) and sets it to nil – rather than copy and pasting this logic.
We’re not setting paused to true before invalidating the display link, as this is redundant.
Instead of force unwrapping the displayLink after checking for non-nil, we’re using optional chaining e.g displayLink?.invalidate() (which will call invalidate() if the display link isn’t nil). While force unwrapping may be ‘safe’ in your given situation (as you’re checking for nil) – it’s potentially unsafe when it comes to future refactoring, as you may re-structure your logic without considering what impact this has on the force unwraps.
We’re clamping the elapsed time to the animation duration in order to ensure that the later animation logic doesn’t produce a value out of the expected range.
Our update method displayLinkDidFire(_:) takes a single argument of type CADisplayLink, as required by the documentation.
I realize this question already has a good answer, but here's another slightly different approach that helps in implementing smooth animations independent of the display link frame rate.
**(Link to demo project available at the bottom of this answer - UPDATE: demo project source code now updated to Swift 4)
For my implementation I opted to wrap the display link in it's own class and setup a delegate reference that will get called with the delta time (the time between the last display link call and the current call) so we can perform our animations a little more smoothly.
I'm currently using this method to animate ~60 views around the screen simultaneously in a game.
First we're going to define the delegate protocol that our wrapper will call to notify of update events.
// defines an interface for receiving display update notifications
protocol DisplayUpdateReceiver: class {
func displayWillUpdate(deltaTime: CFTimeInterval)
}
Next we're going to define our display link wrapper class. This class will take a delegate reference on initialization. When initialized it will automatically start our display link, and clean it up on deinit.
import UIKit
class DisplayUpdateNotifier {
// **********************************************
// MARK: Variables
// **********************************************
/// A weak reference to the delegate/listener that will be notified/called on display updates
weak var listener: DisplayUpdateReceiver?
/// The display link that will be initiating our updates
internal var displayLink: CADisplayLink? = nil
/// Tracks the timestamp from the previous displayLink call
internal var lastTime: CFTimeInterval = 0.0
// **********************************************
// MARK: Setup & Tear Down
// **********************************************
deinit {
stopDisplayLink()
}
init(listener: DisplayUpdateReceiver) {
// setup our delegate listener reference
self.listener = listener
// setup & kick off the display link
startDisplayLink()
}
// **********************************************
// MARK: CADisplay Link
// **********************************************
/// Creates a new display link if one is not already running
private func startDisplayLink() {
guard displayLink == nil else {
return
}
displayLink = CADisplayLink(target: self, selector: #selector(linkUpdate))
displayLink?.add(to: .main, forMode: .commonModes)
lastTime = 0.0
}
/// Invalidates and destroys the current display link. Resets timestamp var to zero
private func stopDisplayLink() {
displayLink?.invalidate()
displayLink = nil
lastTime = 0.0
}
/// Notifier function called by display link. Calculates the delta time and passes it in the delegate call.
#objc private func linkUpdate() {
// bail if our display link is no longer valid
guard let displayLink = displayLink else {
return
}
// get the current time
let currentTime = displayLink.timestamp
// calculate delta (
let delta: CFTimeInterval = currentTime - lastTime
// store as previous
lastTime = currentTime
// call delegate
listener?.displayWillUpdate(deltaTime: delta)
}
}
To use it you simply initialize an instance of the wrapper, passing in the delegate listener reference, then update your animations based on the delta time. In this example, the delegate passes the update call off to the animatable view (this way you could track multiple animating views and have each update their positions via this call).
class ViewController: UIViewController, DisplayUpdateReceiver {
var displayLinker: DisplayUpdateNotifier?
var animView: MoveableView?
override func viewDidLoad() {
super.viewDidLoad()
// setup our animatable view and add as subview
animView = MoveableView.init(frame: CGRect.init(x: 150.0, y: 400.0, width: 20.0, height: 20.0))
animView?.configureMovement()
animView?.backgroundColor = .blue
view.addSubview(animView!)
// setup our display link notifier wrapper class
displayLinker = DisplayUpdateNotifier.init(listener: self)
}
// implement DisplayUpdateReceiver function to receive updates from display link wrapper class
func displayWillUpdate(deltaTime: CFTimeInterval) {
// pass the update call off to our animating view or views
_ = animView?.update(deltaTime: deltaTime)
// in this example, the animatable view will remove itself from its superview when its animation is complete and set a flag
// that it's ready to be used. We simply check if it's ready to be recycled, if so we reset its position and add it to
// our view again
if animView?.isReadyForReuse == true {
animView?.reset(center: CGPoint.init(x: CGFloat.random(low: 20.0, high: 300.0), y: CGFloat.random(low: 20.0, high: 700.0)))
view.addSubview(animView!)
}
}
}
Our moveable views update function looks like this:
func update(deltaTime: CFTimeInterval) -> Bool {
guard canAnimate == true, isReadyForReuse == false else {
return false
}
// by multiplying our x/y values by the delta time new values are generated that will generate a smooth animation independent of the framerate.
let smoothVel = CGPoint(x: CGFloat(Double(velocity.x)*deltaTime), y: CGFloat(Double(velocity.y)*deltaTime))
let smoothAccel = CGPoint(x: CGFloat(Double(acceleration.x)*deltaTime), y: CGFloat(Double(acceleration.y)*deltaTime))
// update velocity with smoothed acceleration
velocity.adding(point: smoothAccel)
// update center with smoothed velocity
center.adding(point: smoothVel)
currentTime += 0.01
if currentTime >= timeLimit {
canAnimate = false
endAnimation()
return false
}
return true
}
If you'd like to look through a full demo project you can download it from GitHub here: CADisplayLink Demo Project
The above is the best example for how to use CADisplayLink with efficiency. Thanks to #Fattie and #digitalHound
I could not resist adding my use of CADisplayLink and DisplayUpdater classes by 'digitalHound' in PdfViewer using WKWebView.
My requirement was to continue auto scroll the pdf at the user selectable speed.
May be the answer here is not correct place, but I intent to show usage of CADisplayLink here. ( for others like me, who can implement their requirement. )
//
// PdfViewController.swift
//
import UIKit
import WebKit
class PdfViewController: UIViewController, DisplayUpdateReceiver {
#IBOutlet var mySpeedScrollSlider: UISlider! // UISlider in storyboard
var displayLinker: DisplayUpdateNotifier?
var myPdfFileName = ""
var myPdfFolderPath = ""
var myViewTitle = "Pdf View"
var myCanAnimate = false
var mySlowSkip = 0.0
// 0.125<=slow, 0.25=normal, 0.5=fast, 0.75>=faster
var cuScrollSpeed = 0.25
fileprivate var myPdfWKWebView = WKWebView(frame: CGRect.zero)
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.title = myViewTitle
let leftItem = UIBarButtonItem(title: "Back", style: .plain, target: self, action: #selector(PdfViewController.PdfBackClick))
navigationItem.leftBarButtonItem = leftItem
self.view.backgroundColor = UIColor.white.cgColor
mySpeedScrollSlider.minimumValue = 0.05
mySpeedScrollSlider.maximumValue = 4.0
mySpeedScrollSlider.isContinuous = true
mySpeedScrollSlider.addTarget(self, action: #selector(PdfViewController.updateSlider), for: [.valueChanged])
mySpeedScrollSlider.setValue(Float(cuScrollSpeed), animated: false)
mySpeedScrollSlider.backgroundColor = UIColor.white.cgColor
self.configureWebView()
let folderUrl = URL(fileURLWithPath: myPdfFolderPath)
let url = URL(fileURLWithPath: myPdfFolderPath + myPdfFileName)
myPdfWKWebView.loadFileURL(url, allowingReadAccessTo: folderUrl)
}
//MARK: - Button Action
#objc func PdfBackClick()
{
_ = self.navigationController?.popViewController(animated: true)
}
#objc func updateSlider()
{
if ( mySpeedScrollSlider.value <= mySpeedScrollSlider.minimumValue ) {
myCanAnimate = false
} else {
myCanAnimate = true
}
cuScrollSpeed = Double(mySpeedScrollSlider.value)
}
fileprivate func configureWebView() {
myPdfWKWebView.frame = view.bounds
myPdfWKWebView.translatesAutoresizingMaskIntoConstraints = false
myPdfWKWebView.navigationDelegate = self
myPdfWKWebView.isMultipleTouchEnabled = true
myPdfWKWebView.scrollView.alwaysBounceVertical = true
myPdfWKWebView.layer.backgroundColor = UIColor.red.cgColor //test
view.addSubview(myPdfWKWebView)
myPdfWKWebView.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor ).isActive = true
myPdfWKWebView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
myPdfWKWebView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
myPdfWKWebView.bottomAnchor.constraint(equalTo: mySpeedScrollSlider.topAnchor).isActive = true
}
//MARK: - DisplayUpdateReceiver delegate
func displayWillUpdate(deltaTime: CFTimeInterval) {
guard myCanAnimate == true else {
return
}
var maxSpeed = 0.0
if cuScrollSpeed < 0.5 {
if mySlowSkip > 0.25 {
mySlowSkip = 0.0
} else {
mySlowSkip += cuScrollSpeed
return
}
maxSpeed = 0.5
} else {
maxSpeed = cuScrollSpeed
}
let scrollViewHeight = self.myPdfWKWebView.scrollView.frame.size.height
let scrollContentSizeHeight = self.myPdfWKWebView.scrollView.contentSize.height
let scrollOffset = self.myPdfWKWebView.scrollView.contentOffset.y
let xOffset = self.myPdfWKWebView.scrollView.contentOffset.x
if (scrollOffset + scrollViewHeight >= scrollContentSizeHeight)
{
return
}
let newYOffset = CGFloat( max( min( deltaTime , 1 ), maxSpeed ) )
self.myPdfWKWebView.scrollView.setContentOffset(CGPoint(x: xOffset, y: scrollOffset+newYOffset), animated: false)
}
}
extension PdfViewController: WKNavigationDelegate {
// MARK: - WKNavigationDelegate
public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
//print("didStartProvisionalNavigation")
}
public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
//print("didFinish")
displayLinker = DisplayUpdateNotifier.init(listener: self)
myCanAnimate = true
}
public func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
//print("didFailProvisionalNavigation error:\(error)")
}
public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
//print("didFail")
}
}
Example Calling from another view is as under.
To load the PDF file from Document folder.
func callPdfViewController( theFileName:String, theFileParentPath:String){
if ( !theFileName.isEmpty && !theFileParentPath.isEmpty ) {
let pdfViewController = self.storyboard!.instantiateViewController(withIdentifier: "PdfViewController") as? PdfViewController
pdfViewController?.myPdfFileName = theFileName
pdfViewController?.myPdfFolderPath = theFileParentPath
self.navigationController!.pushViewController(pdfViewController!, animated: true)
} else {
// Show error.
}
}
This example may be 'modified' to load web page and auto-scroll them at user selected speed.
Regards
Sanjay.
I'm attempting to add support for Voice Over accessibility in a puzzle game which has a fixed board. However, I'm having trouble getting UIAccessibilityElements to show up.
Right now I'm overriding accessibilityElementAtIndex, accessibilityElementCount and indexOfAccessibilityElement in my SKScene.
They are returning an array of accessible elements as such:
func loadAccessibleElements()
{
self.isAccessibilityElement = false
let pieces = getAllPieces()
accessibleElements.removeAll(keepCapacity: false)
for piece in pieces
{
let element = UIAccessibilityElement(accessibilityContainer: self.usableView!)
element.accessibilityFrame = piece.getAccessibilityFrame()
element.accessibilityLabel = piece.getText()
element.accessibilityTraits = UIAccessibilityTraitButton
accessibleElements.append(element)
}
}
Where piece is a subclass of SKSpriteNode and getAccessibilityFrame is defined:
func getAccessibilityFrame() -> CGRect
{
return parentView!.convertRect(frame, toView: nil)
}
Right now one (wrongly sized) accessibility element seems to appear on the screen in the wrong place.
Could someone point me in the right direction?
Many thanks
EDIT:
I've tried a hack-ish work around by placing a UIView over the SKView with UIButton elements in the same location as the SKSpriteNodes. However, accessibility still doesn't want to work. The view is loaded as such:
func loadAccessibilityView()
{
view.isAccessibilityElement = false
view.accessibilityElementsHidden = false
skView.accessibilityElementsHidden = false
let accessibleSubview = UIView(frame: view.frame)
accessibleSubview.userInteractionEnabled = true
accessibleSubview.isAccessibilityElement = false
view.addSubview(accessibleSubview)
view.bringSubviewToFront(accessibleSubview)
let pieces = (skView.scene! as! GameScene).getAllPieces()
for piece in pieces
{
let pieceButton = UIButton(frame: piece.getAccessibilityFrame())
pieceButton.isAccessibilityElement = true
pieceButton.accessibilityElementsHidden = false
pieceButton.accessibilityTraits = UIAccessibilityTraitButton
pieceButton.setTitle(piece.getText(), forState: UIControlState.Normal)
pieceButton.setBackgroundImage(UIImage(named: "blue-button"), forState: UIControlState.Normal)
pieceButton.alpha = 0.2
pieceButton.accessibilityLabel = piece.getText()
pieceButton.accessibilityFrame = pieceButton.frame
pieceButton.addTarget(self, action: Selector("didTap:"), forControlEvents: UIControlEvents.TouchUpInside)
accessibleSubview.addSubview(pieceButton)
}
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil)
}
The buttons are placed correctly, however accessibility just isn't working at all. Something seems to be preventing it from working.
I've searched in vain for a description of how to implement VoiceOver in Swift using SpriteKit, so I finally figured out how to do it. Here's some working code that converts a SKNode to an accessible pushbutton when added to a SKScene class:
// Add the following code to a scene where you want to make the SKNode variable named “leave” an accessible button
// leave must already be initialized and added as a child of the scene, or a child of other SKNodes in the scene
// screenHeight must already be defined as the height of the device screen, in points
// Accessibility
private var accessibleElements: [UIAccessibilityElement] = []
private func nodeToDevicePointsFrame(node: SKNode) -> CGRect {
// first convert from frame in SKNode to frame in SKScene's coordinates
var sceneFrame = node.frame
sceneFrame.origin = node.scene!.convertPoint(node.frame.origin, fromNode: node.parent!)
// convert frame from SKScene coordinates to device points
// sprite kit scene origin is in lower left, accessibility device screen origin is at upper left
// assumes scene is initialized using SKSceneScaleMode.Fill using dimensions same as device points
var deviceFrame = sceneFrame
deviceFrame.origin.y = CGFloat(screenHeight-1) - (sceneFrame.origin.y + sceneFrame.size.height)
return deviceFrame
}
private func initAccessibility() {
if accessibleElements.count == 0 {
let accessibleLeave = UIAccessibilityElement(accessibilityContainer: self.view!)
accessibleLeave.accessibilityFrame = nodeToDevicePointsFrame(leave)
accessibleLeave.accessibilityTraits = UIAccessibilityTraitButton
accessibleLeave.accessibilityLabel = “leave” // the accessible name of the button
accessibleElements.append(accessibleLeave)
}
}
override func didMoveToView(view: SKView) {
self.isAccessibilityElement = false
leave.isAccessibilityElement = true
}
override func willMoveFromView(view: SKView) {
accessibleElements = []
}
override func accessibilityElementCount() -> Int {
initAccessibility()
return accessibleElements.count
}
override func accessibilityElementAtIndex(index: Int) -> AnyObject? {
initAccessibility()
if (index < accessibleElements.count) {
return accessibleElements[index] as AnyObject
} else {
return nil
}
}
override func indexOfAccessibilityElement(element: AnyObject) -> Int {
initAccessibility()
return accessibleElements.indexOf(element as! UIAccessibilityElement)!
}
Accessibility frames are defined in the fixed physical screen coordinates, not UIView coordinates, and transforming between them is kind of tricky.
The device origin is the lower left of the screen, with X up, when the device is in landscape right mode.
It's a pain converting, I've no idea why Apple did it that way.