As you work with iOS and Swift, you’ll doubtless develop a personal library of frequently used convenience functions. Here are some of mine. Every one of them has come in handy in my own life; I keep them available as user snippets in Xcode so that I can paste them into any project.
As I explained in Chapter 1, if an app lacks a main storyboard, or if you want to ignore the main storyboard and generate the app’s initial interface yourself, configuring the window and supplying the root view controller is up to you. A minimal app delegate class would look something like this:
@UIApplicationMain class AppDelegate : UIResponder, UIApplicationDelegate { var window : UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]?) -> Bool { self.window = self.window ?? UIWindow() self.window!.backgroundColor = .white self.window!.rootViewController = ViewController() self.window!.makeKeyAndVisible() return true } }
C and Objective-C have the Core Graphics CGRectMake
function, which needs no argument labels when you call it. Infuriatingly, Swift 3 cuts off access to this function, leaving only the various CGRect initializers, all of which do need argument labels. I find those labels otiose, clumsy, and verbose; we know what each argument signifies, so why clutter the call with labels? The solution is another CGRect initializer, without labels:
extension CGRect { init(_ x:CGFloat, _ y:CGFloat, _ w:CGFloat, _ h:CGFloat) { self.init(x:x, y:y, width:w, height:h) } }
As long as we’re doing that, we may as well supply label-free initializers for the three other common Core Graphics structs:
extension CGSize { init(_ width:CGFloat, _ height:CGFloat) { self.init(width:width, height:height) } } extension CGPoint { init(_ x:CGFloat, _ y:CGFloat) { self.init(x:x, y:y) } } extension CGVector { init (_ dx:CGFloat, _ dy:CGFloat) { self.init(dx:dx, dy:dy) } }
Delayed performance is of paramount importance in iOS programming, where it is often the case that the interface must be given a moment to settle down before we proceed to the next command. Calling asyncAfter
is not difficult, but we can simplify with a utility function:
func delay(_ delay:Double, closure: @escaping ()->()) { let when = DispatchTime.now() + delay DispatchQueue.main.asyncAfter(deadline: when, execute: closure) }
And call it like this:
delay(0.4) { // do something here }
There are no Swift macros (because there’s no Swift preprocessor), so you can’t write the equivalent of Objective-C’s NSDictionaryOfVariableBindings
, which turns a literal list of view names into a dictionary of string names and view references for use in connection with a visual format string in a call to NSLayoutConstraint’s constraints(withVisualFormat:options:metrics:views:)
. You can, however, generate such a dictionary with fixed string names, like this:
func dictionaryOfNames(_ arr:UIView...) -> [String:UIView] { var d = [String:UIView]() for (ix,v) in arr.enumerated() { d["v(ix+1)"] = v } return d }
This utility function takes a list of views and simply makes up new string names for them, of the form "v1"
, "v2"
, and so on, in order. Knowing the rule by which the string names are generated, you then use those string names in your visual format strings. For example, if you generate the dictionary by calling dictionaryOfNames(mainview, myLabel)
, then in any visual format string that uses this dictionary as its views:
dictionary, you will refer to mainview
by the name v1
and to myLabel
by the name v2
.
These are NSLayoutConstraint class methods aimed at helping to detect and analyze constraint issues (referred to in Chapter 1):
extension NSLayoutConstraint { class func reportAmbiguity (_ v:UIView?) { var v = v if v == nil { v = UIApplication.shared.keyWindow } for vv in v!.subviews { print("(vv) (vv.hasAmbiguousLayout)") if vv.subviews.count > 0 { self.reportAmbiguity(vv) } } } class func listConstraints (_ v:UIView?) { var v = v if v == nil { v = UIApplication.shared.keyWindow } for vv in v!.subviews { let arr1 = vv.constraintsAffectingLayout(for:.horizontal) let arr2 = vv.constraintsAffectingLayout(for:.vertical) NSLog(" %@ H: %@ V:%@", vv, arr1, arr2); if vv.subviews.count > 0 { self.listConstraints(vv) } } } }
A recurring pattern in Cocoa is that a value class instance is created and configured beforehand for a one-time use. Here’s a case in point:
let para = NSMutableParagraphStyle() para.headIndent = 10 para.firstLineHeadIndent = 10 para.tailIndent = -10 para.lineBreakMode = .byWordWrapping para.alignment = .center para.paragraphSpacing = 15 content.addAttribute( NSParagraphStyleAttributeName, value:para, range:NSMakeRange(0,1))
That has a clunky, procedural feel. It would be clearer and more functional, as well as reflecting the natural order of thought, if the creation and configuration of para
could happen just at the actual moment when we need this object, namely when we supply the value:
argument. Here’s a generic function that permits us to do that:
func lend<T> (_ closure: (T)->()) -> T where T:NSObject { let orig = T() closure(orig) return orig }
Now we can express ourselves like this:
content.addAttribute(NSParagraphStyleAttributeName, value:lend { (para:NSMutableParagraphStyle) in para.headIndent = 10 para.firstLineHeadIndent = 10 para.tailIndent = -10 para.lineBreakMode = .byWordWrapping para.alignment = .center para.paragraphSpacing = 15 }, range:NSMakeRange(0,1))
My original goal was to encapsulate the clunky, boilerplate, imperative-programming dance of beginning an image graphics context, drawing into it, extracting the image, and ending the context. In its place, I wrote a utility function that did everything but the drawing, which would be provided as a function parameter. Then iOS 10 introduced UIGraphicsImageRenderer, which works in exactly that way. But now there’s a new problem: what if you want backward compatibility? So now my utility function combines the old implementation with the new one:
func imageOfSize(_ size:CGSize, opaque:Bool = false, closure: () -> ()) -> UIImage { if #available(iOS 10.0, *) { let f = UIGraphicsImageRendererFormat.default() f.opaque = opaque let r = UIGraphicsImageRenderer(size: size, format: f) return r.image {_ in closure()} } else { UIGraphicsBeginImageContextWithOptions(size, opaque, 0) closure() let result = UIGraphicsGetImageFromCurrentImageContext()! UIGraphicsEndImageContext() return result } }
You call it like this (using my label-free Core Graphics initializers, of course):
let im = imageOfSize(CGSize(100,100)) { let con = UIGraphicsGetCurrentContext()! con.addEllipse(in: CGRect(0,0,100,100)) con.setFillColor(UIColor.blue.cgColor) con.fillPath() }
This is a solution to the problem, posed in Chapter 4, of how to repeat a view animation a fixed number of times without using “begin/commit” syntax. My approach is to employ tail recursion and a counter to chain the individual animations. The delay
call unwinds the call stack and works around possible drawing glitches:
extension UIView { class func animate(times:Int, duration dur: TimeInterval, delay del: TimeInterval, options opts: UIViewAnimationOptions, animations anim: @escaping () -> Void, completion comp: ((Bool) -> Void)?) { func helper(_ t:Int, _ dur: TimeInterval, _ del: TimeInterval, _ opt: UIViewAnimationOptions, _ anim: @escaping () -> Void, _ com: ((Bool) -> Void)?) { UIView.animate(withDuration: dur, delay: del, options: opt, animations: anim, completion: { done in if com != nil { com!(done) } if t > 0 { delay(0) { helper(t-1, dur, del, opt, anim, com) } } }) } helper(times-1, dur, del, opts, anim, comp) } }
The calling syntax is exactly like ordering a UIView animation in its full form, except that there’s an initial times
parameter:
let opts = UIViewAnimationOptions.autoreverse let xorig = self.v.center.x UIView.animate(times:3, duration:1, delay:0, options:opts, animations:{ self.v.center.x += 100 }, completion:{ _ in self.v.center.x = xorig })
It is often convenient to collect the indexes of items to be deleted from an array, and then to delete those items. We must be careful to sort the indexes in decreasing numeric order first, because array indexes will be off by one after an item at a lower index is removed:
extension Array { mutating func remove(at ixs:Set<Int>) -> () { for i in Array<Int>(ixs).sorted(by:>) { self.remove(at:i) } } }
13.58.212.170