Autoresizing table cells

July 25th, 2016

Extending the autoresizing of UITextView to table cells, this is how that looks.

First, let’s create a trivial model class and a storage class for that model:

class Item: NSObject {

  var text: String

    

  init(text: String) {

    self.text = text

    super.init()

  }    

}


class ItemStore {

  var allItems = [Item]()

    

  func createItem(text: String) -> Item {

    let newItem = Item(text: text)

    allItems.append(newItem)

    return newItem

  }

    

  init() {

    for_in 0..<20 {

      createItem(“For ever and ever, this may be a great filler text. We will never know with absolute certainty, will we? Horray!”)

    }

  }

}


As you can see, I create 20 items all with the same text, just to be able to fill more than a screen in the table view. In the main storyboard, delete the default window and replace it all with a table view controller. Subclass the controller:

import UIKit

 

protocol RefreshableTableViewController {

  func refresh()

}

 

class MyTableViewController: UITableViewController, RefreshableTableViewController {

  var itemStore: ItemStore!

    

  override func viewDidLoad() {

    super.viewDidLoad()

        

    // move down the table view contents a bit

        

    let statusBarHeight = UIApplication.sharedApplication().statusBarFrame.height

    let insets = UIEdgeInsets(top: statusBarHeight, left: 0, bottom: 0, right: 0)

    tableView.contentInset = insets

    tableView.scrollIndicatorInsets = insets

        

    tableView.rowHeight = UITableViewAutomaticDimension

    tableView.estimatedRowHeight = 65

  }

    

  override func tableView(tableView: UITableView,

               numberOfRowsInSection section: Int) -> Int {

    return itemStore.allItems.count

  }

    

  override func tableView(tableView: UITableView,

                cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

    let cell = tableView.dequeueReusableCellWithIdentifier(“ItemCell”,

                forIndexPath: indexPath) as! ItemCell

        

    // cell needs a delegate link back to the tableView controller, so it

    // can trigger refresh of row heights

    cell.tableController = self

        

    // hand the item to the cell. as it updates the item, the model store

    // is automatically kept up to date since it holds a reference to the item

    let item = itemStore.allItems[indexPath.row]

    cell.useItem(item)

        

    return cell

  }

 

  func refresh() {

    // after updating the size and constraints of the table view cell,

    // the table itself

    // must refresh to adjust the distance between the rows

        

    tableView.beginUpdates()

    tableView.endUpdates()

  }

    

  override func didRotateFromInterfaceOrientation(

                 fromInterfaceOrientation: UIInterfaceOrientation) {

    // updating all constraints and table rows after a rotation requires the heavy

    // hand of a full reload

        

    tableView.reloadData()

  }    

}



From the application delegate, create the store, get the view controller and feed it the store:

  func application(application: UIApplication, didFinishLaunchingWithOptions

             launchOptions: [NSObject: AnyObject]?) -> Bool {

    // Override point for customization after application launch.

        

    let itemStore = ItemStore()

        

    let itemController = window!.rootViewController as! MyTableViewController

    itemController.itemStore = itemStore

        

    return true

  }


Now, subclass UITableViewCell:

class ItemCell: UITableViewCell, UITextViewDelegate {

    

  @IBOutlet var textView: UITextView!

  @IBOutlet var heightConstraint: NSLayoutConstraint!

 

  var lastCalculatedTextHeight: CGFloat = 0.0

  var tableController: RefreshableTableViewController?

  var item: Item!     // must be class for reference semantics

    

  func useItem(item: Item) {

    self.item = item

    self.textView.text = item.text

    updateMeasurements()

  }

    

  func updateMeasurements() {

    let newHeight = calculateHeightTextView(textView, forText: textView.text)

    heightConstraint.constant = newHeight

  }

 

  func textViewDidChange(textView: UITextView) {

    // update the model with the current text

    item.text = textView.text

  }

    

  func textView(textView: UITextView, shouldChangeTextInRange range: NSRange,

                    replacementText text: String) -> Bool {

    // it’s important that we measure the text and set the constraint *before*

    // the textview actually sets the new

    // text, else the scroll view will jitter and jump.

    // that’s why we catch “shouldChange” to get in ahead of that

        

    let newText = buildNewText(textView.text, range: range, replacementText: text)

    let newHeight = calculateHeightTextView(textView, forText: newText)

    refreshIfNeeded(newHeight)

    return true

  }

    

  func refreshIfNeeded(newHeight: CGFloat) {

    if newHeight != lastCalculatedTextHeight {

      lastCalculatedTextHeight = newHeight

      heightConstraint.constant = newHeight

      tableController?.refresh()

    }

  }

    

  func buildNewText(oldText: String, range: NSRange,

                   replacementText: String) -> String {

    if let nsRange = oldText.rangeFromNSRange(range) {

      let newText = oldText.stringByReplacingCharactersInRange(nsRange,

                   withString: replacementText)

      return newText

    }

    // if the range was invalid, just return the old text

    return oldText

  }

    

  func calculateHeightTextView(textView: UITextView, forText: String) -> CGFloat {

        

    if let font = textView.font {

      let padding = textView.textContainer.lineFragmentPadding

      let width = textView.contentSize.width – 2 * padding

            

      let attributes = [NSFontAttributeName: font]


      let newRect = forText.boundingRectWithSize(

               CGSize(width: width, height: CGFloat.max),

               options: [.UsesLineFragmentOrigin, .UsesFontLeading],

               attributes: attributes, context: nil)

      let textInset = textView.textContainerInset

      let newHeight = newRect.height + textInset.bottom + textInset.top

            

      return newHeight

    }

 

    // if we don’t know what else to say, just return the old height,

    // probably better than nothing

    NSLog(“failed to calculate height, no font?”)

    return textView.contentSize.height

  }

}


This cell is designed in IB as the prototype cell. It needs to get constraints as follows:

XcodeScreenSnapz001
 
The height constraint on the text view needs to get a priority less than 1000, e.g. 999, else it will give a conflict with a constraint that iOS will add on the fly when instantiating the cell. Interestingly, even though your own height constraint gets a lower priority than the system created constraint, still it will work fine.
XcodeScreenSnapz002
 
You then need to bind the height constraint to the outlet in the ItemCell, and the text view to its outlet in the ItemCell, as well. The result looks like in this very brief movie I posted on Vimeo.
 
Interestingly, the size change happens with a smooth animation even though I didn’t animate the change in the size of the text view as I had to do in the previous project. The animation originates in the table view itself, so it comes for free.
 
Because there are a number of things you need to set correctly in Interface Builder, and some details I may have glossed over here, I’ve posted the entire project for your downloading pleasure. Again, as in the previous post, this is all Swift 2.x and XCode 7.3.
 


Autoresizing UITextView

July 24th, 2016

A problem I needed to solve was: while editing in a text view, I want the view to grow and shrink depending on the text entered. I also want this to work in conjunction with auto-layout.

Autosizing UITextView

This is something I’ve been fighting for some time, but now I sat down and spent enough time (and quite a bit of Stack Overflow reading) to get it to work. The first problem I kept having was finding all the parameters that determine the actual size of the text area so I could calculate it reliably. The other important problem was that most everything I tried made the text scroll up a little bit every now and then, making for an ugly jumping up and down of the whole text content. The solution I found by trial and error (mostly error) was to calculate and reset the height constraint before the text view actually set the new text, so it has to be done in shouldChangeTextInRange delegate method.

class ViewController: UIViewController, UITextViewDelegate {

 

  @IBOutlet weak var heightConstraint: NSLayoutConstraint!

  @IBOutlet weak var myTextView: UITextView!

 

  var lastCalculatedTextHeight: CGFloat = 0.0

    

  override func viewDidLoad() {

    super.viewDidLoad()

    self.resizeTextView(myTextView)

  }

 

  func resizeTextView(textView: UITextView) {

    resizeTextView(textView, forText: textView.text)

  }

    

  func resizeTextView(textView: UITextView, forText newText: String) {

        

    if let font = textView.font {

            

      let padding = textView.textContainer.lineFragmentPadding

      let width = textView.contentSize.width – 2 * padding

            

      let attributes = [NSFontAttributeName: font]

            

      let newRect = newText.boundingRectWithSize(

                     CGSize(width: width, height: CGFloat.max),

                     options: [.UsesLineFragmentOrigin, .UsesFontLeading],

                     attributes: attributes, context: nil)

 

      let textInset = textView.textContainerInset

      let newHeight = newRect.height + textInset.bottom + textInset.top

 

      if newHeight != lastCalculatedTextHeight {

        lastCalculatedTextHeight = newHeight

        UIView.animateWithDuration(0.5, animations: {

          self.heightConstraint.constant = newHeight

          self.view.layoutIfNeeded()

        })

      }

    }

  }

 

  // by doing this, by capturing the new size of the textView *before* the

  // text contents actually change, we can avoid having

  // the scroll view jump. We resize it before it sees it needs to grow.

    

  func textView(textView: UITextView, shouldChangeTextInRange range: NSRange,

                           replacementText text: String) -> Bool {

    let oldText = textView.text

    if let nsRange = oldText.rangeFromNSRange(range) {

      let newText = oldText.stringByReplacingCharactersInRange(nsRange,

                    withString: text)

      resizeTextView(textView, forText: newText)

    }

    return true

  }

}


You also need an extension on String for this:

import Foundation

 

extension String {

  func rangeFromNSRange(nsRange: NSRange) -> Range<String.Index>? {

    let from16 = utf16.startIndex.advancedBy(nsRange.location, limit: utf16.endIndex)

    let to16 = from16.advancedBy(nsRange.length, limit: utf16.endIndex)

    if let from = String.Index(from16, within: self),

             to = String.Index(to16, within: self) {

      return from ..< to

    }

    return nil

  }

    

  func NSRangeFromRange(range: Range<String.Index>) -> NSRange {

    let from = String.UTF16View.Index(range.startIndex, within: utf16)

    let to = String.UTF16View.Index(range.endIndex, within: utf16)

    return NSMakeRange(utf16.startIndex.distanceTo(from), from.distanceTo(to))

  }

}

 

All of this is for Swift 2.x, by the way. Ah, another little neat thing: you put a height constraint on the text view through IB, then connect that constraint to an outlet. That’s the “heightConstraint” outlet at the top of the listing. (I never even thought of that before.)

You can find the whole project in a little zip-file.

Next little project is putting this to work in table cells, or maybe stack containers.

Medical IT crap, the why

July 11th, 2016

(Continuing from my previous post.)

I think the major problem is that buyers specify domain functionality, but not the huge list of “non-functional requirements”. So anyone fulfilling the functional requirements can sell their piece of crap as lowest bidder.

Looking at a modern application, non-functional requirements are stuff like resilience, redundancy, load management, the whole security thing, but also cut-and-paste in a myriad of formats, a number of import and export data formats, ability to quick switch between users, ability to save state and transfer user state from machine to machine, undo/redo, accessibility, error logging and fault management, adaptive user interface layouts, and on and on.

I’d estimate that all these non-functional requirements can easily be the largest part of the design and development of a modern application, but since medical apps are, apparantly, never specified with any of that, they’re artificially cheap, and, not to mince words, a huge pile of stinking crap.

It’s really easy to write an app that does one thing, but it’s much harder and more expensive to write an app that actually works in real environments and in conjunction with other applications. So, this is on the purchasers’ heads. Mainly.

A day in the life of “medical IT security”

July 9th, 2016

This article is an excellent description of some of the serious problems related to IT security in healthcare.

Even though medical staff actively circumvent “security” in a myriad inventive ways, it’s pretty clear that 99% of the blame lies with IT staff and vendors being completely out of touch with the actual institutional mission. To be able to create working and useable systems, you *must* understand and be part of the medical work. So far, I’ve met very few technologists even remotely interested in learning more about the profession they’re ostensibly meant to be serving. It boggles the mind, but not in a good way.

Some quotes:

“Unfortunately, all too often, with these tools, clinicians cannot do their job—and the medical mission trumps the security mission.”

“During a 14-hour day, the clinician estimated he spent almost 1.5 hours merely logging in.”

“…where clinicians view cyber security as an annoyance rather than as an essential part of patient safety and organizational mission.”

“A nurse reports that one hospital’s EMR prevented users from logging in if they were already logged in somewhere else, although it would not meaningfully identify where the offending session was.” 

This one, I’ve personally experienced when visiting another clinic. Time and time again. You then have to call back to the office and ask someone to reboot or even unplug the office computer, since it’s locked to my account and noone at the office is trusted with an admin password… Yes, I could have logged out before leaving, assuming I even knew I was going to be called elsewhere then. Yes, I could log out every time I left the office, but logging in took 5-10 minutes. So screen lock was the only viable solution.

“Many workarounds occur because the health IT itself can undermine the central mission of the clinician: serving patients.”

“As in other domains, clinicians would also create shadow systems operating in parallel to the health IT.”

Over here, patients are given full access to medical records over the ‘net, which leads physicians to write down less in the records. Think this through to its logical conclusion…

Server-side Swift

July 7th, 2016

This presentation from WWDC 2016 boggles the mind. It completely overturned all my concepts about server-side nodejs and javascript in general. If you’re into docker containers or anything of the kind, and you develop in Swift client-side, this must be seen.

Let’s hope the project doesn’t die. Let’s hope I didn’t overestimate this.

Somewhat dumb credit card region lock

June 16th, 2016

Visa has a neat feature where you can determine in which regions the card can be used. In my case, it’s “internet”, “Sweden”, “Nordic countries”, “Europe”, “North and central America”, “South America”, “Africa”, “Asia”, “Oceania”. You can set these through the credit card app (mine is from Volvo, of course).

So I disabled all regions except “Internet” and “Sweden”, planning on enabling other regions when I travel. 

Today I got a message from Netflix that they couldn’t charge my card. No explanation why. I called the card issuer and after some digging they explained to me that since I disabled “Europe”, Netflix got refused. Turns out that Netflix charges from region “Europe”, not “Internet”. More specifically from The Netherlands. Once I reenabled “Europe”, the charge went through.

Now, there are several problems with this. First of all, an internet based service like Netflix should be in the region “Internet”. Secondly, if it isn’t in “Internet”, they should at the very least tell us from which region they charge. I had no idea Netflix charges from The Netherlands. How could I? It’s not reasonable to expect us to check with the card issuer every time this happens, and have them go dig through logs (took them 10 minutes to find, so it wasn’t trivial).

Worst of all, this kind of thing implies that you’d better open up a lot of regions you’re not travelling to, since you don’t know from which regions different internet based companies do their charging.

Having the card processor issue meaningful error messages, not just “sorry we failed”, would definitely help a lot, too.

Now I hate Microsoft even more, part II

June 10th, 2016

Started my Win 10 instance under Parallels, and just one minute into working with it, I got this:

Parallels DesktopScreenSnapz095

That gave me 8 minutes to get my stuff in order. The time when this showed up was 21:18. So I clicked “Close” and just got the close box on my accounting program and the system rebooted. So much for the eight minutes, which turned into more like 10 seconds.

Note, BTW, no way to postpone this at all. Nothing.

The evil and arrogant fuckers.

Oh, lest you think Microsoft really let me save correctly, I got this after restart from my accounting program:

Parallels DesktopScreenSnapz096

It says, in Swedish: “The company wasn’t closed correctly. The company will now be optimized.” Meaning the index files will be rebuilt. 

Microsoft really doesn’t give a shit about our data. Fortunately, I hadn’t started entering anything so the rebuild worked out fine.

Sabotage for the office

June 10th, 2016

Interesting manual on sabotage from the CIA. The last five pages describe some offices I’ve worked in.

Apple quality control needs work

June 4th, 2016

Just wasted several hours trying to find out why home sharing stopped working on my Apple TV. I’ve got the one with optical audio output, can’t remember if that is called the gen 2 or 3, but you know which one I mean.

Duck-ducked it thoroughly, finding a truckload of similar complaints over the last two years, which in itself wasn’t too encouraging. Most recommended logging out and in from home sharing, changing the computer name in system settings, and so on. Nothing helped.

Finally I changed the wireless from my very current tower Airport Extreme to a slightly older, flat square, Airport Extreme, and lo, all the misery resolved itself. Which reminded me that the Extreme did an update maybe two days ago.

I’m getting increasingly bad vibes about Apple quality, or lack thereof. 

This is why the internet is so slow

May 22nd, 2016

Got an html file from someone who created it with MS Office. In a browser, it shows one line of text. In the source, it’s 759 lines of html.

Jeez…