The tvOS long press

Random

If you’d like to detect a long press on a UIButton or just for a view that has no buttons – it’s pretty easy. There is one simple gotcha, however. You’ll trigger your selector twice. Once when detected (down on the glass pad Apple TV remote), and again on the pad’s release.

So you’ll need to set a BOOL for knowing what the state is. Easy enough.

fileprivate var longDown: Bool = false

override func viewDidLoad()
{
    super.viewDidLoad()
    let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, 
    action: #selector(longPress(longPressGestureRecognizer:)))
    view.addGestureRecognizer(longPressGestureRecognizer)
}

func longPress(longPressGestureRecognizer : UILongPressGestureRecognizer)
{
    // I fire on the down and also on the release.
        
    if longDown == false {
        longDown = true
        print("long press down.")
    } else {
        longDown = false
        print("long press up.")
    }
}

I would not have thought that the gesture would be triggered twice on hardware, but it does.

Tagged :

WWDC 17 Packing List

WWDC17

It’s almost time for WWDC17, this year in San Jose, CA instead of San Francisco. I haven’t been in several years because my lottery skills are obviously lacking. I managed to score one this year and I’m pretty excited. What will I take this year?

Well, to be honest, taking a laptop to sessions in the past, for me, has been serious overkill. I might jot some notes down in a Moleskine or other notebook – for transcription later. Taking notes on a laptop brings distraction to the game for me. Am I plugged in and charging? I need to get close to a plug. Oh, I can check Facebook during this session. Email. Pretend to be coding something amazing in Xcode. Taking notes on a laptop makes me feel like a court stenographer and I want to relax a bit and take the information in. I can always watch the session again later in its stored video format (to catch some details that might have gone by too quickly).

I will still take my trusty MacBook Pro R, but that will be for when I’m back in my room at night – coding, emailing, sorting photos, watching session videos again, etc. It will probably stay in my room locked up. Charged and ready for my welcome return at the close of each day. That will save me a bunch of weight to lug around too. I can use my laptop bag for other things, including hoofing multiple Odwalla juices around (if they supply those big cans of it around the venue).

For travel:

  • Bose QuietControl 30
  • Bose QuietComfort 35
  • Casio ProTrek PRW 3100y-1B (all black) watch

For the sessions:

  • iPhone 6 Plus. The most important bit of gear I have.
  • At least two portable battery chargers. Anker Astro Pro Series(20000mAH) and a RavPower (16750mAH)one I had before the Anker. Both charged up and ready to fly. The Anker needs plugged into an outlet, the RavPower does microUSB for charging.
  • Two point-and-shoot cameras – both Sony. An older one which is my favorite, and a newer model which I don’t like as much.
  • Timbuk2 messenger laptop bag.
  • Moleskine, fountain pen, EDC pens, etc.
  • Zojirushi Coffee Thermos 20oz. Über awesome and über important.
  • Various cables, USB wall plugs, etc.
  • Takeya ThermoFlask Insulated Stainless Steel Water Bottle, 40 oz, Asphalt. Huge. Perfect. Might leave at the hotel as it could be overkill.
  • Apple Watch series 2. I’m hoping Apple updates the WWDC iOS app and it gets watch support – for keeping us on our selected schedules, getting important updates, map support, etc.
  • Sunglasses. For me, very important.

Hotel:

  • Takeya ThermoFlask Insulated Stainless Steel Water Bottle, 40 oz, Asphalt. Huge. Perfect. No water bottles.
  • Couple of polos/shirts (a few older WWDC ones, a Swift logo one, etc.)
  • Shorts/jeans/Scarpa shoes. Scarpa Margherita rock.
  • A spring jacket in case it gets cool at night (Patagonia fold up for laptop bag) – not wearing a WWDC17 one if Apple swags those out.
  • Livionex tooth gel. Its amazing.
  • Wet shaving and beard gear. Very important to clean up daily.
  • Packing cubes. Although I’m not bringing a ton of clothes, they really, really, really help keep things tidy. I know I’ll be bringing back a few additional shirts and a sweatshirt at least.
  • Snooz white noise generator (I got from Kickstarter)
Tagged : / / /

Getting hour offsets for local time versus a target time zone in Swift

Swift

I am currently working on something that will display time in different time zones. However, you need to compare the user’s current time versus those at the target time zones to be accurate. This is what I figured out.

It’s easy to get the user’s local time.

let cal = Calendar(identifier: .gregorian)
let date = Date()
let hour = cal.component(.hour, from: date)
let minute = cal.component(.minute, from: date)
let second = cal.component(.second, from: date)
print("\nLOCAL TIME: \(hour):\(minute):\(second)\n")

Now, you need to get the time in other time zone targets. Here is an example for London, England. A continuation from the code above.

let locale = TimeZone.init(identifier: "Europe/London")
let comp = cal.dateComponents(in: locale!, from: date)
print("LONDON: \(String(describing: comp.hour!)):\(String(describing: comp.minute!)):\(String(describing: comp.second!)), day: \(comp.day!)")

Excellent. Now, compute the difference between the user’s local time and the time in London.

// I am in Boston currently.
print("London offset (modifier): \(comp.hour! - hour)") // 5

And there you have it. The difference is 5 hours ahead (it’s a positive value). If I were in London or that time zone, the difference would be 0. For someone like me who rarely dabbles in date work, this was a bit of discovered magic. For others, this post must be quite pedestrian and a waste of time. I apologize. But if I found a post like this earlier, I could have saved myself some time.

Tagged : /

tvOS UIFocusGuide demystified

A post about tvOS UIFocus enlightenment and a helper Class that I use to help debug my user interface work.

UIFocus

Above you’ll notice four buttons (a screenshot from my actual Apple TV). You’ll also notice a total of eight purple boxes with dashed outlines. Each is labeled in all capitals. Those are instances of my helper Class (“FocusGuideRepresentation”). You see, UIFocusGuides do not have any visual interface. So when you are deploying them, you’re essentially working in the dark. This helper Class shows you right where the guides are to help you visually lay everything out. Of course, when you get into dynamic situations where buttons are shuffling around, this can help you even more.

The focus management for tvOS works incredibly well when your buttons line up vertically or horizontally. It just works, you don’t need to do anything for that functionality. When they aren’t aligned, you can get into situations where buttons aren’t available through normal navigation. Above, without any UIFocusGuides, a user could move from button One to Two. And back up. That would be it. Buttons Three and Four would be hung out to dry without the user able to navigate to them. That’s why UIFocusGuides exist.

You can think of them as invisible buttons that pass focus to an actual button – based on rules that you provide.

I decided that in addition to being to move up and down with the Siri remote to access the buttons, left and right should also work at the top. A user swiping right from One to get to Three should work. That means 8 guides, as you can see in the diagram. The dashed rule lines show how each guide passes focus. That is a lot of guides, but in the end, the navigation ends up being buttery and simple to use. An application should be a joy to use.

Below is the code for the helper Class, followed by the full code for what you see in the image. Try it out on an Apple TV and see what’s going on and experience how nice it feels getting around.

import UIKit

class FocusGuideRepresentation: UIView {

    init(frameSize: CGRect, label: String)
    {
        super.init(frame: frameSize)
        self.backgroundColor = UIColor.blue.withAlphaComponent(0.1)
        let myLabel = UILabel(frame: CGRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height))
        myLabel.font = UIFont.systemFont(ofSize: 20)
        myLabel.textColor = UIColor.white.withAlphaComponent(0.5)
        myLabel.textAlignment = .center
        myLabel.text = label.uppercased()
        self.addSubview(myLabel)
        
        // Add a dashed rule around myself.
        
        let border = CAShapeLayer()
        border.strokeColor = UIColor.white.withAlphaComponent(0.4).cgColor
        border.fillColor = nil
        border.lineWidth = 1
        border.lineDashPattern = [4, 4]
        border.path = UIBezierPath(rect: self.bounds).cgPath
        border.frame = self.bounds
        self.layer.addSublayer(border)
    }
    
    required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
}

And, now, the main code for the ViewController. Note: the 4 UIButtons are in the Storyboard and hooked up (as you see the @IBOutlets).

import UIKit

class ViewController: UIViewController {

    @IBOutlet var one:   UIButton!
    @IBOutlet var two:   UIButton!
    @IBOutlet var three: UIButton!
    @IBOutlet var four:  UIButton!

    var fg1: FocusGuideRepresentation!
    var fg2: FocusGuideRepresentation!
    var fg3: FocusGuideRepresentation!
    var fg4: FocusGuideRepresentation!
    var fg5: FocusGuideRepresentation!
    var fg6: FocusGuideRepresentation!
    var fg7: FocusGuideRepresentation!
    var fg8: FocusGuideRepresentation!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setUpFocusGuides()
        
        // Pop these back up above the guide representations.
        
        self.view.addSubview(one)
        self.view.addSubview(two)
        self.view.addSubview(three)
        self.view.addSubview(four)
    }

    func setUpFocusGuides()
    {
        let firstFocusGuide = UIFocusGuide()
        view.addLayoutGuide(firstFocusGuide)
        firstFocusGuide.leftAnchor.constraint(equalTo:   one.leftAnchor).isActive =    true
        firstFocusGuide.topAnchor.constraint(equalTo:    one.bottomAnchor).isActive =  true
        firstFocusGuide.heightAnchor.constraint(equalTo: one.heightAnchor).isActive =  true
        firstFocusGuide.widthAnchor.constraint(equalTo:  three.widthAnchor).isActive = true
        firstFocusGuide.preferredFocusEnvironments = [three]
        
        let secondFocusGuide = UIFocusGuide()
        view.addLayoutGuide(secondFocusGuide)
        secondFocusGuide.rightAnchor.constraint(equalTo:  three.rightAnchor).isActive =  true
        secondFocusGuide.bottomAnchor.constraint(equalTo: three.topAnchor).isActive =    true
        secondFocusGuide.heightAnchor.constraint(equalTo: three.heightAnchor).isActive = true
        secondFocusGuide.widthAnchor.constraint(equalTo:  three.widthAnchor).isActive =  true
        secondFocusGuide.preferredFocusEnvironments = [one]
        
        let thirdFocusGuide = UIFocusGuide()
        view.addLayoutGuide(thirdFocusGuide)
        thirdFocusGuide.leftAnchor.constraint(equalTo:   two.leftAnchor).isActive =   true
        thirdFocusGuide.bottomAnchor.constraint(equalTo: two.topAnchor).isActive =    true
        thirdFocusGuide.heightAnchor.constraint(equalTo: two.heightAnchor).isActive = true
        thirdFocusGuide.widthAnchor.constraint(equalTo:  four.widthAnchor).isActive = true
        thirdFocusGuide.preferredFocusEnvironments = [four]

        let fourthFocusGuide = UIFocusGuide()
        view.addLayoutGuide(fourthFocusGuide)
        fourthFocusGuide.leftAnchor.constraint(equalTo:   four.leftAnchor).isActive =   true
        fourthFocusGuide.topAnchor.constraint(equalTo:    four.bottomAnchor).isActive = true
        //fourthFocusGuide.bottomAnchor.constraint(equalTo: two.bottomAnchor).isActive =  true
        fourthFocusGuide.heightAnchor.constraint(equalTo: four.heightAnchor).isActive = true
        fourthFocusGuide.widthAnchor.constraint(equalTo:  four.widthAnchor).isActive =  true
        fourthFocusGuide.preferredFocusEnvironments = [two]
        
        let fifthFocusGuide = UIFocusGuide()
        view.addLayoutGuide(fifthFocusGuide)
        fifthFocusGuide.leftAnchor.constraint(equalTo:   three.leftAnchor).isActive =   true
        fifthFocusGuide.bottomAnchor.constraint(equalTo: one.bottomAnchor).isActive =  true
        fifthFocusGuide.heightAnchor.constraint(equalTo: three.heightAnchor).isActive = true
        fifthFocusGuide.widthAnchor.constraint(equalTo:  three.widthAnchor).isActive =  true
        fifthFocusGuide.preferredFocusEnvironments = [three]
        
        let sixthFocusGuide = UIFocusGuide()
        view.addLayoutGuide(sixthFocusGuide)
        sixthFocusGuide.leftAnchor.constraint(equalTo:   one.leftAnchor).isActive =   true
        sixthFocusGuide.bottomAnchor.constraint(equalTo: three.bottomAnchor).isActive =  true
        sixthFocusGuide.heightAnchor.constraint(equalTo: three.heightAnchor).isActive = true
        sixthFocusGuide.widthAnchor.constraint(equalTo:  three.widthAnchor).isActive =  true
        sixthFocusGuide.preferredFocusEnvironments = [one]
        
        let seventhFocusGuide = UIFocusGuide()
        view.addLayoutGuide(seventhFocusGuide)
        seventhFocusGuide.leftAnchor.constraint(equalTo:   four.leftAnchor).isActive =   true
        seventhFocusGuide.bottomAnchor.constraint(equalTo: two.bottomAnchor).isActive =  true
        seventhFocusGuide.heightAnchor.constraint(equalTo: four.heightAnchor).isActive = true
        seventhFocusGuide.widthAnchor.constraint(equalTo:  four.widthAnchor).isActive =  true
        seventhFocusGuide.preferredFocusEnvironments = [four]
        
        let eighthFocusGuide = UIFocusGuide()
        view.addLayoutGuide(eighthFocusGuide)
        eighthFocusGuide.leftAnchor.constraint(equalTo:   two.leftAnchor).isActive =   true
        eighthFocusGuide.bottomAnchor.constraint(equalTo: four.bottomAnchor).isActive =  true
        eighthFocusGuide.heightAnchor.constraint(equalTo: four.heightAnchor).isActive = true
        eighthFocusGuide.widthAnchor.constraint(equalTo:  four.widthAnchor).isActive =  true
        eighthFocusGuide.preferredFocusEnvironments = [two]
        
        // To aid in debug placement.
        
        fg1 = FocusGuideRepresentation(frameSize: firstFocusGuide.layoutFrame, label:  "first")
        fg2 = FocusGuideRepresentation(frameSize: secondFocusGuide.layoutFrame, label: "second")
        fg3 = FocusGuideRepresentation(frameSize: thirdFocusGuide.layoutFrame, label:  "third")
        fg4 = FocusGuideRepresentation(frameSize: fourthFocusGuide.layoutFrame, label: "fourth")
        fg5 = FocusGuideRepresentation(frameSize: fifthFocusGuide.layoutFrame, label: "fifth")
        fg6 = FocusGuideRepresentation(frameSize: sixthFocusGuide.layoutFrame, label: "sixth")
        fg7 = FocusGuideRepresentation(frameSize: seventhFocusGuide.layoutFrame, label: "seventh")
        fg8 = FocusGuideRepresentation(frameSize: eighthFocusGuide.layoutFrame, label: "eighth")
        
        self.view.addSubview(fg1)
        self.view.addSubview(fg2)
        self.view.addSubview(fg3)
        self.view.addSubview(fg4)
        self.view.addSubview(fg5)
        self.view.addSubview(fg6)
        self.view.addSubview(fg7)
        self.view.addSubview(fg8)
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

 

Tagged : / /

Creating a tvOS parallax UIButton

Image Stack

If you’ve been involved in tvOS application development, or you’re new to the whole process, you might find this post interesting in regards to user interface.

I have recently been involved in tvOS dabbling. I’ve been creating a custom application (no TVML) and I wanted one of those nifty parallax-style buttons that Apple uses in its media browsing (notably the TV application). You can focus an item and it will shift neatly as you move around on the Siri remote control. Perfect if you don’t need to include explanatory text. However creating one eluded me for a short while. There may be a better way to handle this, but I present to you my working solution.

TV Stack

Create an Apple TV Image Stack

You need to create a new Apple TV Image Stack in Xcode (when you have your Assets.xcassets folder selected).

The stack takes 3 files by default. I used a background white PNG, a shadow PNG, and a product PNG. If you don’t supply a solid background image, you’ll see the shadow behind your UIButton – which doesn’t look good. Drag in your images into each layer (Front, Middle, Back). You’ll get a nice preview of the parallax at the top so you can see how your images work together. This allows for you to determine if you need to make changes to get the look that you want. I’ve shown each image at the top of this post, each box representing one of them. I named mine “qc35Stack” – which you’ll need to refer to later as an image name.

stacks

Create your custom UIButton with parallax

Now that you have your image stack, you can use it to supply images for a UIButton’s various states. When you supply a stack in this way, the button will know that it should perform a parallax presentation. I created the 3 images at the same size that I want the buttons to be (i.e. the image sizes match the UIButton’s declared frame).

Here is the code I settled on.

let buttonUnpressedTexture = UIImage(named: "qc35Stack")
let buttonPressedTexture = UIImage(named: "qc35Stack")
let newButton = UIButton(type: .custom)
newButton.frame = CGRect(x: 800, y: 200, width: 180, height: 180)
newButton.setImage(buttonUnpressedTexture, for: UIControlState.normal)
newButton.setImage(buttonPressedTexture, for: UIControlState.highlighted)
newButton.setImage(UIImage(named: "qc35Stack"), for: UIControlState.focused)
newButton.imageView?.clipsToBounds = false
newButton.imageView?.adjustsImageWhenAncestorFocused = true
newButton.adjustsImageWhenHighlighted = true

Tada

Finished.

A note: I first attempted to use static PNG images for the highlighted and normal states of the UIButton, but the transitions between them and the focused were glitchy and didn’t look good. When I used the same stack for each of those states, things look good. I don’t know if this is the correct way to do it or not, but it is working.

Go ahead and try it out for yourself. It seems to work a treat, and you didn’t need to subclass a UIButton in your own Class to get it working either.

A short post because the solution is pretty simple.

Richer text for UILabels

Swift

This post is silly simple, but in the past, I remember doing things like this using Ranges.  You have a 2-line UILabel and you want a bold font for the first line, and then regular for the second. In Swift, this is quite simple and straight-forward. This is all the code you’d need to pull it off easily.

let style = NSMutableParagraphStyle()
style.lineSpacing = 5
let attString = NSMutableAttributedString(string: "I am the first line.\n",
    attributes: [NSFontAttributeName: UIFont(name:"Gotham-Bold", size:18.0)!,
    NSParagraphStyleAttributeName: style])
attString.append(NSMutableAttributedString(string: "I am the second line.",
    attributes: [NSFontAttributeName: UIFont(name:"Gotham-Book", size:18.0)!,
    NSParagraphStyleAttributeName: style]))
label.attributedText = attString