Some apps want to support alternative app icons in Settings where user can choose to update app icon. Here’s some must do to make it work, as of Xcode 12.2
In Info.plist, must declare CFBundleIcons with both CFBundlePrimaryIcon and CFBundleAlternateIcons
Icons must be in project folder, not Asset Catalog
Prepare icons, like Icon1, Icon2 with 2 variants for 2x and 3x. These need to be added to project folder. When I add these to Asset Catalog it does not update app icon. I usually add a new folder in project like Resource/AlternateAppIcons
The default 1x size for iPhone icon size from iOS 7 to iOS 14 is 60px.
Although declaring CFBundlePrimaryIcon seems unnecessary, I find that without this it does not work. I use AppIcon60x60 as the compiled one from Asset Catalog for our main AppIcon
For UIPrerenderedIcon, a boolean value indicating whether the app’s icon already contains a shine effect. We set to false to allow iOS to apply round and glossy effect to our icon.
Since we’re using Asset Catalog, if we simply use UIImage with named, it will load by default in Asset Catalog, unless we manually specify bundle path. For simple demo, we can just copy our images again to Asset Catalog so we can show in the app
I usually have an enum of AlternateAppIcon so I can display in a grid and let user choose
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
enumAlternateAppIcon: String, CaseIterable, Identifiable{ var id: AlternateAppIcon { self }
case main = "Main" case pride1 = "Pride1" case pride2 = "Pride2" }
There’s Menu button it shows a dropdown style. We fake it by fading this and overlay with a button. allowsHitTesting does not work, but disabled seems to do the trick
From iOS 13, the default is to support multiple scene, so the the old UIApplicationDelegate lifecycle does not work. Double check your Info.plist for UIApplicationSceneManifest key
From the API, looks like Binding<Set> is for multiple selection, and Binding<Optional> is for single selection Looking at List signature, I see that selection uses wrappedValue as Set for Binding<Set<SelectionValue>>?
As someone who builds lots of apps, I try to find quick ways to do things. One of them is to avoid repetitive and cumbersome APIs. That’s why I built Anchors to make Auto Layout more convenient, Omnia to add missing extensions. The next thing in the itchy list is the many ways to communicate among objects in iOS and macOS development that can be very annoying sometimes.
This post is my take on tackling some communication patterns issue and how to make it simpler with EasyClosure. The learning applies for both iOS and macOS development, and code is in Swift 5.
The more complex the app, the more we need to split responsibilities for classes and make them communicate. Target action is not the only way for objects communication, there are delegates, notification center, KVO and blocks. All of these are used all over the placed within iOS SDKs. Not only do we have to be aware of syntax differences, but we also have to care about how and when to use one over another.
This article Communication Patterns from objc.io is my favorite and many of the principles still hold true for now.
As I do many apps, I’m a bit baffled by cumbersome APIs, I wish I could write code in a more succinct and convenient way. Something like:
button.on.tap { print("button was tapped" }
user.on.propertyChange(\.name) { print("name has changed to \($0)"}
tableView.on.tapRow { print("row \($0) was tapped" }
This may seem a bit overengineered and some of you may feel OK with the iOS APIs. I have to admit that I prefer explicit over the clever code, but in this case, we can make the APIs look a bit nicer. This is achieved with a less known featured in ObjC Runtime called associated objects.
In the old Objective C age, there was the famous BlocksKit which allows us to deal with UIKit/AppKit in the handy block syntax. Although Objective C block is hard to declare, they are much more declarative than other communication patterns like delegate or handling UIAlert actions. And with the closure in Swift, we can do even nicer things.
Associated objects
With Objective C category and Swift extension, we can’t add a stored property, that’s where associated objects come into play. The associated object allows us to attach other objects to the lifetime of any NSObject , with these 2 free functions objc_getAssociatedObject and objc_setAssociatedObject .
The associated object lies in the Objective C runtime category and can be considered hacky, but it has shown to be very effective in some scenarios.
Detecting object deallocation
Since the associated object is attached to the host object, there’s this very handy use case that allows detection of the life cycle of the host object. We can, for example, observe UIViewControllerto tell if it has really been deallocated.
class Notifier {
deinit {
print("host object was deinited")
}
}
extension UIViewController {
private struct AssociatedKeys {
static var notifier = "notifier"
}
var notifier: Notifier? {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.notifier) as? Notifier
}
set {
if let newValue = newValue {
objc_setAssociatedObject(
self,
&AssociatedKeys.notifier,
newValue as Notifier?,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
)
}
}
}
}
Then if we at some time set the host object to nil, the associated object will be deallocated too, giving us a callback to handle to deinit event:
If we’re gonna make the on extension to UIButton, UITableView, UIViewController we have to add the associated object into NSObject for all these classes to have on property.
extension NSObject {
private struct AssociatedKeys {
static var on = "on"
}
var on: On? {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.on) as? On
}
set {
if let newValue = newValue {
objc_setAssociatedObject(
self,
&AssociatedKeys.on,
newValue as On?,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
)
}
}
}
}
This brings another problem with code completion, we can act on UIButton but Xcode still hints us about all methods on On , but for UIButton only tap and propertyChange are valid. The textChange is more for UITextField and UITextView:
button.on.textChange {}
Protocol with associated type
To remedy this awkward API, we can use a very sweet feature of Swift called protocol with an associated type. We start by introducing EasyClosureAware that has a host EasyClosureAwareHostType of type AnyObject. This means that this protocol is for any class that wants to attach itself to a host object.
private struct AssociatedKey {
static var key = "EasyClosure_on"
}
public protocol EasyClosureAware: class {
associatedtype EasyClosureAwareHostType: AnyObject
var on: Container<EasyClosureAwareHostType> { get }
}
extension EasyClosureAware {
public var on: Container<Self> {
get {
if let value = objc_getAssociatedObject(self, &AssociatedKey.key) as? Container<Self> {
return value
}
let value = Container(host: self)
objc_setAssociatedObject(self, &AssociatedKey.key, value, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return value
}
}
}
extension NSObject: EasyClosureAware { }
Then we confirm NSObject to EasyClosureAware so every NSObject subclass has the on property for free.
The Container is for containing all targets and to keep all target alive. With this approach we can wrap around any patterns like delegate, target action and KVO observer.
public class Container<Host: AnyObject>: NSObject {
public unowned let host: Host
public init(host: Host) {
self.host = host
}
// Keep all targets alive
public var targets = [String: NSObject]()
}
With this setup, we can easily apply to any object. For example UIButton:
We have ButtonTarget that acts as target for target-action for UIButton , which inherits from UIControl .
Now to react to button tap, it’s as simple as calling:
button.on.tap {}
And Xcode shows correct auto-completion. If we’re about to use UITextField , there’s no code suggestions showing up as there’s no methods for UITextField yet:
textField.on. // ummh?
We need to add a method to Container that has UITextField constraint, for example:
I’ve used this technique extensively and it works on any platform like iOS, macOS, tvOS as they all base on Objective C Runtime and NSObject. We can easily extend it to any classes we want. This can replace target action, delegate, notification center, KVO or any other communication patterns.
In the next sections, let’s explore timer, KVO and notification and whether we should have our on closure.
Action to RxSwift observable
Having button.on.tap {} is nice, but it would be great if that can transform into Observablefor some RxSwift fans like me.
We can have our own RxButton like:
final class RxButton: UIButton {
let tap = PublishSubject<()>()
override init(frame: CGRect) {
super.init(frame: frame)
self.on.tap { tap.onNext(()) }
}
}
We use PublishSubject to map from the imperative to the declarative world of Rx, then we can consume it:
button.tap.subscribe(onNext: {})
Timer keeps a strong reference to its target
Unlike target-action in UIControl where target is held weakly, Timer keeps its target strongly to deliver tick event.
If you are using init(timeInterval:target:selector:userInfo:repeats:) then please read the section about target carefully.
The object to which to send the message specified by aSelector when the timer fires. The timer maintains a strong reference to this object until it (the timer) is invalidated.
Now that Timer gets better API, our on property on Timer is no longer needed, which is fine.
Key-value observing in Swift 5
Key-value observing is the ability to watch property change for an NSObject , before Swift 5 the syntax is quite verbose and error-prone with addObserver and observeValuem methods. This is not to mention the usage of context , especially in the subclassing situation, where we need a context key to distinguish between observations of different objects on the same keypath.
public extension Container where Host: NSObject {
func observe(object: NSObject, keyPath: String, _ action: [@escaping](http://twitter.com/escaping) AnyAction) {
let item = KeyPathTarget.Item(object: object, keyPath: keyPath, action: action)
keyPathTarget.items.append(item)
object.addObserver(keyPathTarget, forKeyPath: keyPath, options: .new, context: nil)
}
func unobserve(object: NSObject, keyPath: String? = nil) {
let predicate: (KeyPathTarget.Item) -> Bool = { item in
return item.object === object
&& (keyPath != nil) ? (keyPath! == item.keyPath) : true
}
keyPathTarget.items.filter(predicate).forEach({
object.removeObserver(keyPathTarget, forKeyPath: $0.keyPath)
})
keyPathTarget.items = keyPathTarget.items.filter({ !predicate($0) })
}
}
class KeyPathTarget: NSObject {
class Item {
let object: NSObject
let keyPath: String
let action: AnyAction
init(object: NSObject, keyPath: String, action: [@escaping](http://twitter.com/escaping) AnyAction) {
self.object = object
self.keyPath = keyPath
self.action = action
}
}
var items = [Item]()
deinit {
items.forEach({ item in
item.object.removeObserver(self, forKeyPath: item.keyPath)
})
items.removeAll()
}
// MARK: - KVO
override func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?) {
guard let object = object as? NSObject,
let keyPath = keyPath,
let value = change?[.newKey] else {
return
}
let predicate: (KeyPathTarget.Item) -> Bool = { item in
return item.object === object
&& keyPath == item.keyPath
}
items.filter(predicate).forEach({
$0.action(value)
})
}
}
Then we can have an observer to observe contentSize of UIScrollView like:
let observer = NSObject()
observer.on.observe(object: scrollView: keyPath: #keyPath(UIScrollView.contentSize)) { value in print($0 as? CGSize)}
Starting from Swift 5, there’s an introduction of KeyPath syntax and improvement to KVO. Now we can just:
@objc class User: NSObject {
@objc dynamic var name = "random"
}
let thor = Person()
thor.observe(\User.name, options: .new) { user, change in
print("User has a new name \(user.name)")
}
thor.name = "Thor"
As for KVO, we need to mark @objc and dynamic for it to work. The rest is just to call observe on the object with the KeyPath we want to observe.
Block-based Notification Center
NotificationCenter is the mechanism to post and receive notification system-wide. Starting from iOS 4, NotificationCenter got its block API addObserverForName:object:queue:usingBlock:
The only thing to notice is that block parameter being copied.
The block to be executed when the notification is received. The block is copied by the notification center and (the copy) held until the observer registration is removed.
As for EasyClosure, to wrap around NotficationCenter is easy:
public extension Container where Host: NSObject {
func observe(notification name: Notification.Name,
_ action: [@escaping](http://twitter.com/escaping) NotificationAction) {
let observer = NotificationCenter.default.addObserver(
forName: name, object: nil,
queue: OperationQueue.main, using: {
action($0)
})
notificationTarget.mapping[name] = observer
}
func unobserve(notification name: Notification.Name) {
let observer = notificationTarget.mapping.removeValue(forKey: name)
if let observer = observer {
NotificationCenter.default.removeObserver(observer as Any)
notificationTarget.mapping.removeValue(forKey: name)
}
}
}
class NotificationTarget: NSObject {
var mapping: [Notification.Name: NSObjectProtocol] = [:]
deinit {
mapping.forEach({ (key, value) in
NotificationCenter.default.removeObserver(value as Any)
})
mapping.removeAll()
}
}
And with its on extension:
viewController.on.observe(notification: Notification.Name.UIApplicationDidBecomeActive) { notification in
print("application did become active")
}
viewController.on.unobserve(notification: Notification.Name.UIApplicationDidBecomeActive)
This is for demonstration purpose only as the default NotificationCenter with block-based API is good enough. There are some cautions when using it NSNotificationCenter with blocks considered harmful that’s good to be aware of.
Where do we go from here
We’ve learned how to make use of associated objects and make nicer APIs. EasyClosure is designed to be extensible and we can wrap any communication patterns. KVO and NotificationCenter APIs have become better starting iOS 10 and Swift 5, and we see a trend of more closure based API as they are declarative and convenient. When we can, we should stick to the system APIs as much as possible and only make our sugar when needed.
I hope you find this article helpful, here ‘s a fun gif made with EasyClosure APIs:
func allOn() -> Bool {
return [good, cheap, fast].filter({ $0.isOn }).count == 3
}
good.on.valueChange { _ in
if allOn() {
fast.setOn(false, animated: true)
}
}
cheap.on.valueChange { _ in
if allOn() {
good.setOn(false, animated: true)
}
}
fast.on.valueChange { _ in
if allOn() {
cheap.setOn(false, animated: true)
}
}
Auto Layout has been around since macOS 10.7 and iOS 6.0 as a nicer way to do layouts over the old resizing masks. Besides some rare cases when we need to manually specify origins and sizes, Auto Layout is the preferred way to do declarative layouts whether we choose to do UI in code or Storyboard. The Auto Layout APIs have some improvements over the years, there are also some sugar libraries that add easy syntaxes, so more choices for developers.
In this article, let’s take a review of how layout has improved over the years, from manual layout, autoresizing masks and finally to Auto Layout. I will mention a few takes on some libraries and how to abstract Auto Layout using the builder pattern. There are also some notes about the view life cycle and APIs improvements that might be overlooked. These are of course based on my experience and varies because of personal tastes but I hope you‘ll find something useful.
Although the article mention iOS, the same learning applies to watchOS, tvOS and macOS too.
Positioning a view before Auto Layout
When I first started iOS programming in early 2014, I read a book about Auto Layout and that book detailed lots of scenarios that completely confused me. It didn’t take long until I tried Auto Layout in an app and I realized it w as so simple. In its simplest sense, a view needs a position and a size to be correctly shown on the screen, everything else is just extra. In Auto Layout’s term, we need to specify enough constraints to position and size the view.
Manual layout using CGRect
If we take a look back at the way we do a manual layout with the frame, there are origins and sizes. For example, here is how to position a red box that stretches accordingly with the width of the view controller.
viewDidLoad is called when the view property of view controller is loaded, we need to wait until viewDidLayoutSubviews so that we have access to the final bounds. When the bounds change for a view controller’s view, the view adjusts the positions of its subviews and then the system calls this method.
Why is view size correct in viewDidLoad
viewDidLoad is definitely not the recommended way to do manual layout but there are times we still see that its view is correctly sized and fills the screen. This is when we need to read View Management in UIViewController more thoroughly:
Each view controller manages a view hierarchy, the root view of which is stored in the *view*property of this class. The root view acts primarily as a container for the rest of the view hierarchy. The size and position of the root view is determined by the object that owns it, which is either a parent view controller or the app’s window. The view controller that is owned by the window is the app’s root view controller and its view is sized to fill the window. A view controller’s root view is always sized to fit its assigned space.
It can also be that the view has fixed size in xib or storyboard but we should control the size explicitly and do that in the right view controller method to avoid unexpected behaviors.
Autoresizing masks
Autoresizing mask was the old way to make layout a bit more declarative, also called springs and struts layout system. It is integer bit mask that determines how the receiver resizes itself when its superview’s bounds change. Combining these constants lets you specify which dimensions of the view should grow or shrink relative to the superview. The default value of this property is none, which indicates that the view should not be resized at all.
While we can specify which edges we want to fix and which should be flexible, it is confusing in the ways we do in xib and in code.
In the above screenshot, we pin the top of the red box to the top of the screen, and that is fixed distance. When the view changes size, the width and height of the red box changes proportionally, but the top spacing remains fixed.
In code, instead of specifying autoresizing in terms of fixed distance, we use flexible terms to specify which edges should be flexible.
To achieve the same red box on the top of the screen, we need to specify a flexible width and a flexible bottom margin. This means the left, right and top edges are fixed.
Horizontally fixed distance from the left: [.flexibleRightMargin]
Center horizontally [.flexibleLeftMargin, .flexibleRightMargin]
Vertically fixed distance from the top: [.flexibleBottomMargin]
Center vertically [.flexibleTopMargin, .flexibleBottomMargin]
These are not very intuitive and the way these are scaled proportionally may not fit our expectation. Also, note that multiple options can be done on the same axis.
When more than one option along the same axis is set, the default behavior is to distribute the size difference proportionally among the flexible portions. The larger the flexible portion, relative to the other flexible portions, the more it is likely to grow. For example, suppose this property includes the flexibleWidth and flexibleRightMarginconstants but does not include the flexibleLeftMargin constant, thus indicating that the width of the view’s left margin is fixed but that the view’s width and right margin may change.
Understanding of autoresizing masks won’t waste your time, we will come back to it in a few minutes 😉
Auto Layout to the rescue
Auto Layout, together with dynamic text and size classes, are recommended ways to build adaptive user interfaces as there the number of iOS devices with different screen sizes grows.
A constraint-based layout system
Auto Layout is described via NSLayoutConstraint by defining constraints between 2 objects. Here is the simple formula to remember:
Here is how to replicate that red box with NSLayoutConstraint. We need to specify which property of which view should be connected to another property of another view. Auto Layout supports many attributes such ascenterX, centerY and topMargin.
If we run the above code, we will get into the popular warning message regarding translatesAutoresizingMaskIntoConstraints
[LayoutConstraints] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want.
Try this:
(1) look at each constraint and try to figure out which you don't expect;
(2) find the code that added the unwanted constraint or constraints and fix it.
(Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints)
(
"<NSAutoresizingMaskLayoutConstraint:0x600003ef2300 h=--& v=--& UIView:0x7fb66c5059f0.midX == 0 (active)>",
"<NSLayoutConstraint:0x600003e94b90 H:|-(20)-[UIView:0x7fb66c5059f0](LTR) (active, names: '|':UIView:0x7fb66c50bce0 )>"
)
If you need some help deciphering this, there is wtfautolayout that does a good job on explaining what’s really happening.
It is said that resizing masks has been reimplemented using Auto Layout under the hood, and there is always NSAutoresizingMaskLayoutConstraint added to the view, hence the midX constraint.
We should never mix resizing masks and Auto Layout to avoid unwanted behavior, the fix is simply to disable translatesAutoresizingMaskIntoConstraints
This property is false by default for views from xib or storyboard but holds true if we declare layout in code. The intention is for the system to create a set of constraints that duplicate the behavior specified by the view’s autoresizing mask. This also lets you modify the view’s size and location using the view’s frame, bounds, or center properties, allowing you to create a static, frame-based layout within Auto Layout.
Visual Format Language
The Visual Format Language lets you use ASCII-art like strings to define your constraints. I see it is used in some code bases so it’s good to know it.
Here is how to recreate the red box using VFL. We need to specify constraints for both horizontal and vertical direction. Note that the same format string may result in multiple constraints:
1 2 3 4 5 6
let views = ["box" : box] let horizontal = "H:|-20-[box]-20-|" let vertical = "V:|-50-[box(100)]" let horizontalConstraints = NSLayoutConstraint.constraints(withVisualFormat: horioptions: [], metrics: nil, views: views) let verticalConstraints = NSLayoutConstraint.constraints(withVisualFormat: veoptions: [], metrics: nil, views: views) NSLayoutConstraint.activate([horizontalConstraints, verticalConstraints].flatMap({ $0 }))
Visual Format Language is a bit nicer than the verbose NSLayoutConstraint initializer, but it encourages string format, which is error-prone.
addConstraint and activate
This may seem trivial but I see in modern code bases, there is still usage of addConstraint . This was old and hard to use, as we must find the nearest common ancestor view of the 2 views that envolve in Auto Layout.
Starting from iOS 8, there is isActive and the static activate functions that ease this adding constraints process a lot. Basically what it does is to activate or deactivate the constraint with calls toaddConstraint(_:) and removeConstraint(_:) on the view that is the closest common ancestor of the items managed by this constraint.
NSLayoutAnchor
Starting with macOS 10.11 and iOS 9, there was NSLayoutAnchor that simplifies Auto Layout a lot. Auto Layout was declarative, but a bit verbose, now it is simpler than ever with an anchoring system.
The cool thing about NSLayoutAnchor is its generic constrained API design. Constraints are divided into X-axis, Y-axis and dimension anchor type that makes it hard to make mistakes.
open class NSLayoutXAxisAnchor : NSLayoutAnchor<NSLayoutXAxisAnchor>
open class NSLayoutDimension : NSLayoutAnchor<NSLayoutDimension>
For example, we can’t pin the top anchor to the left anchor as they are in different axis, and it makes no sense. Attempt to do the following results in compilation issue as Swift strong type system ensures correctness.
I’ve gone into a few cases where the error messages from NSLayoutAnchor don’t help. If we mistakenly connect topAnchor with centerXAnchor, which are not possible as they are from different axes.
Xcode is complaining about ‘Int’ is not convertible to ‘CGFloat’ which is very misleading. Can you spot the error?
The problem is that we are using equalToConstant , not equalTo . The generic constraints of NSLayoutAnchor is giving us misleading errors and can waste lots of time of us trying to figure out the subtle typos.
Abstractions over Auto Layout
NSLayoutAnchor is getting more popular now but it is not without any flaws. Depending on personal taste, there might be some other forms of abstractions over Auto Layout, namely Cartography and SnapKit, which I ‘ve used and loved. Here are a few of my takes on those.
Cartography
Cartography is one of the most popular ways to do Auto Layout in iOS. It uses operators which makes constraints very clear:
What I don’t like about Cartography is that we have to repeat parameter names and that the parameters inside closure are just proxies, not real views and there is a limit on the number of constrain items.
Another huge con was that long compilation time issue due to excessive use of operators Very long compile time in large functions. Although Swift compilation time is getting better, this was a big problem. I even had to write a script to remove Cartography to use simple NSLayoutAnchor, so take a look at AutoLayoutConverter, which converts Cartography code from
There are always tradeoffs but to reduce compilation time at the time was a top priority.
SnapKit
SnapKit, originally Masonry, is perhaps the most popular Auto Layout wrapper
box.snp.makeConstraints { (make) -> Void in
make.width.height.equalTo(50)
make.center.equalTo(self.view)
}
The syntax is nice and with snp namespace to avoid extension name clashes, which I love.
The thing I don’t like about SnapKit is that limited closure. We can only work on 1 view at a time, and the make inside closure is just a proxy, which does not seem intuitive.
Imagine if we’re gonna make paging views or piano, where each view is stacked side by side. We need a lot of SnapKit calls as we can only work on 1 view at a time. Also, there is no clear relationship where we connect with the other view.
We might exclude more than one properties or set different priority levels for each constraint. The simple wrapper with overloading functions and default parameters are just like building rigid abstraction based on premature assumptions. This just limits us in the long run and not scalable 😢
Embracing Auto Layout
All the Auto Layout frameworks out there are just convenient ways to build NSLayoutConstraint, in fact, these are what you normally need
Call addSubview so that view is in the hierarchy
Set translatesAutoresizingMaskIntoConstraints = false
Set isActive = true to enable constraints
Here is how to make an extension on NSLayoutConstraint that disables translatesAutoresizingMaskIntoConstraints for the involved views. Code is from Omnia
Here before we activate constraints, we find the firstItem then disables translatesAutoresizingMaskIntoConstraints. From Swift 4.2 there is a separation between compactMap and flatMap so we can safely use flatMap to flatten an array of arrays. This is useful when we have an array of arrays of constraints.
This is a very thin but powerful wrapper over NSLayoutAnchor and we can expand it the way we need. It sadly has some problems, like we can’t easily change the priority, as we have to reference the constraint 😢
Making Auto Layout more convenient with the builder pattern
The above extension on NSLayoutConstraint works well. However, if you’re like me who wants even more declarative and fast Auto Layout code, we can use the builder pattern to make Auto Layout even nicer. The builder pattern can be applied to many parts of the code but I find it very well suited for Auto Layout. The final code is Anchors on GitHub, and I will detail how to make it.
Here is what we want to achieve to quickly position 4 views:
Most of the times, we want to anchor to parent view, so that should be implicitly done for us. I like to have anchor namespace to avoid extension naming clashes and to make it the starting point for all our convenient Auto Layout code. Let’s identify a few core concepts
Which objects can interact with Auto Layout?
Currently, there are 3 types of objects that can interact withAuto Layout
UIView
UILayoutSupport, from iOS 7, for UIViewController to get bottomLayoutGuide and topLayoutGuide . In iOS 11, we should use safeAreaLayoutGuide from UIView instead
UILayoutGuide: using invisible UIView to do Auto Layout is expensive, that’s why Apple introduced layout guides in iOS 9 to help.
So to support these 3 with anchor namespace, we can make Anchor object that holds AnyObject as behind the scene, NSLayoutConstraint works with AnyObject:
public class Anchor: ConstraintProducer {
let item: AnyObject
/// Init with View
convenience init(view: View) {
self.init(item: view)
}
/// Init with Layout Guide
convenience init(layoutGuide: LayoutGuide) {
self.init(item: layoutGuide)
}
// Init with Item
public init(item: AnyObject) {
self.item = item
}
}
Now we can define anchor property
public extension View {
var anchor: Anchor {
return Anchor(view: self)
}
}
public extension LayoutGuide {
var anchor: Anchor {
return Anchor(layoutGuide: self)
}
}
Which properties are needed in a layout constraint?
The builder patterns make building things declaratively by holding temporary values. Besides from, to, priority, identifier, we need an array of pins to handle cases where there are multiple created constraints. A center constraint results in both centerX and centerY constraints, and an edge constraint results in top, left, bottom and right constraints.
enumTo{ case anchor(Anchor) case size casenone } classPin{ let attribute: Attribute var constant: CGFloa init(_ attribute: Attribute, constant: CGFloat = 0) { self.attribute = attribute self.constant = constant } let item: AnyObject // key: attribute // value: constant var pins: [Pin] = [ var multiplierValue: CGFloat = 1 var priorityValue: Float? var identifierValue: String? var referenceBlock: (([NSLayoutConstraint]) -> Void)? var relationValue: Relation = .equal var toValue: To = .none
With this, we can also expand our convenient anchor to support more constraints, like spacing horizontally, which adds left and right constraints with correct constants. Because as you know, in Auto Layout, for right and bottom direction, we need to use negative values:
There are times we want to infer constraints, like if we want a view’s height to double its width. Since we already have width, declaring ratio should pair the height to the width.
I see we’re used to storing constraint property in order to change its constant later. The constraints property in UIView has enough info and it is the source of truth, so retrieving constraint from that is more preferable.
Here‘s how we find constraint and update that
boxA.anchor.find(.height)?.constant = 100
// later
boxB.anchor.find(.height)?.constant = 100
// later
boxC.anchor.find(.height)?.constant = 100
The code to find constraint is very straightforward.
One of the patterns I see all over is resetting constraints in UITableViewCell or UICollectionViewCell. Depending on the state, the cell removes certain constraints and add new constraints. Cartography does this well by using group.
If we think about it, NSLayoutConstraint is just layout instructions. It can be activated or deactivated . So if we can group constraints, we can activate or deactivate them as a whole.
Here is how to declare 4 groups of constraints, the syntax is from Anchors but this applies to NSLayoutAnchor as well since those generateNSLayoutConstraint under the hood.
let g1 = group(box.anchor.top.left)
let g2 = group(box.anchor.top.right)
let g3 = group(box.anchor.bottom.right)
let g4 = group(box.anchor.bottom.left)
Where to go from here
In this article, we step a bit in time into manual layout, autoresizing masks and then to the modern Auto Layout. The Auto Layout APIs have improvements over the years and are recommended way to do layout. Learning declarative layout also helps me a lot when I learn Constraint Layout in Android, flexbox in React Native or the widget layout in Flutter.
The post goes through the detailed implementation of how we can build more convenient Auto Layout like Anchors with the builder pattern. In the next article, let’s explore the many ways to debug Auto Layout and how to correctly do Auto Layout for different screen sizes.
In the meantime, let’s play Tetris in Auto Layout, because why not 😉
From my previous post How to use flexible frame in SwiftUI we know that certain views have different frame behaviors. 2 of them are .overlay and GeometryReader that takes up whole size proposed by parent.
By default GeometryReader takes up whole width and height of parent, and align its content as .topLeading
Use this method to specify a fixed size for a view’s width, height, or both. If you only specify one of the dimensions, the resulting view assumes this view’s sizing behavior in the other dimension.
Always specify at least one size characteristic when calling this method. Pass nil or leave out a characteristic to indicate that the frame should adopt this view’s sizing behavior, constrained by the other non-nil arguments.
The size proposed to this view is the size proposed to the frame, limited by any constraints specified, and with any ideal dimensions specified replacing any corresponding unspecified dimensions in the proposal.
If no minimum or maximum constraint is specified in a given dimension, the frame adopts the sizing behavior of its child in that dimension. If both constraints are specified in a dimension, the frame unconditionally adopts the size proposed for it, clamped to the constraints. Otherwise, the size of the frame in either dimension is:
If a minimum constraint is specified and the size proposed for the frame by the parent is less than the size of this view, the proposed size, clamped to that minimum.
If a maximum constraint is specified and the size proposed for the frame by the parent is greater than the size of this view, the proposed size, clamped to that maximum.
Otherwise, the size of this view.
Experiment with different proposed frame
To understand the explanation above, I prepare a Swift playground to examine with 3 scenarios: when both minWidth and maxWidth are provided, when either minWidth or maxWidth is provided. I use width for horizontal dimension but the same applies in vertical direction with height.
I have a View called Examine to demonstrate flexible frame. Here we have a flexible frame with red border and red text showing its size where you can specify minWidth and maxWidth.
Inside it is the content with a fixed frame with blue border and blue text showing content size where you can specify contentWidth. Finally there’s parentWidth where we specify proposed width to our red flexible frame.
The variations for our scenarios are that proposed width falls outside and inside minWidth, contentWidth, and maxWidth range.
Here are the results with different variations of specifying parentWidth aka proposed width.
🍑 Scenario 1: both minWidth and maxWidth are specified
Our red flexible frame clamps proposed width between its minWidth and maxWidth, ignoring contentWidth
1
let redWidth = clamp(minWidth, parentWidth, maxWidth)
🍅 Scenario 2: only minWidth is specified
Our red flexible frame clamps proposed width between its minWidth and contentWidth. In case content is less than minWidth, then final width is minWidth
1
let redWidth = clamp(minWidth, parentWidth, contentWidth)
🍏 Scenario 3: only maxWidth is specified
Our red flexible frame clamps proposed width between its contentWidth and maxWidth. In case content is more than maxWidth, then final width is maxWidth
1
let redWidth = clamp(contentWidth, parentWidth, maxWidth)
What are idealWidth and idealHeight
In SwiftUI, view takes proposed frame from its parent, then proposes its to its child, and reports the size it wants from it’s child and its proposed frame from parent. The reported frame is the final frame used by that view.
When we use .frame modifier, SwiftUI does not changes the frame of that view directly. Instead it creates a container around that view.
There are 4 kinds of frame behavior depending on which View we are using. Some have mixed behavior.
Sum up frames from its children then report the final frame. For example HStack, VStack
Merely use the proposed frame. For example GeometryReader, .overlay, Rectangle
Use more space than proposed. For example texts with fixedSize
Use only space needed for its content and respect proposed frame as max
Fix the size to its ideal size
Some View like Text or Image has intrinsic content size, means it has implicit idealWidth and idealHeight. Some like Rectangle we need to explicit set .frame(idealWidth: idealHeight). And these ideal width and height are only applied if we specify fixedSize
Fixes this view at its ideal size. During the layout of the view hierarchy, each view proposes a size to each child view it contains. If the child view doesn’t need a fixed size it can accept and conform to the size offered by the parent. For example, a Text view placed in an explicitly sized frame wraps and truncates its string to remain within its parent’s bounds:
1 2 3
Text("A single line of text, too long to fit in a box.") .frame(width: 200, height: 200) .border(Color.gray)
The fixedSize() modifier can be used to create a view that maintains the ideal size of its children both dimensions:
1 2 3 4
Text("A single line of text, too long to fit in a box.") .fixedSize() .frame(width: 200, height: 200) .border(Color.gray)
You can think of fixedSize() as the creation of a counter proposal to the view size proposed to a view by its parent. The ideal size of a view, and the specific effects of fixedSize() depends on the particular view and how you have configured it.
To view this in playground, I have prepared this snippet
1 2 3 4 5 6 7 8 9 10 11 12 13 14
structText_Previews: PreviewProvider{ staticvar previews: some View { VStack(spacing: 16) { Text("A single line of text, too long to fit in a box.") .fixedSize() .border(Color.red) .frame(width: 200, height: 200) .border(Color.gray) } .padding() .frame(width: 500, height: 500)
} }
Here we can see that our canvas is 500x500, and the Text grows outside its parent frame 200x200
Play with Rectangle
Remember that shapes like Rectangle takes up all the proposed size. When we explicitly specify fixedSize, theidealWidth and idealHeight are used.
Here I have 3 rectangle
🍎 Red: There are no ideal size explicitly specified, so SwiftUI uses a magic number 10 as the size 🍏 Green: We specify frame directly and no idealWidth, idealHeight and no fixedSize, so this rectangle takes up full frame 🧊 Blue: The outer gray box has height 50, but this rectangle uses idealWidth and idealHeight of 200 because we specify fixedSize
NSTextView has this handy method to make scrollable NSTextView NSTextView.scrollableTextView(). The solution is to get to the responder outside enclosing NSScrollView, in my case it is the SwiftUI hosting view
1 2 3 4 5 6 7 8 9
classDisabledScrollTextView: NSTextView{ overridefuncscrollWheel(with event: NSEvent) { // 1st nextResponder is NSClipView // 2nd nextResponder is NSScrollView // 3rd nextResponder is NSResponder SwiftUIPlatformViewHost self.nextResponder?.nextResponder?.nextResponder?.scrollWheel(with: event) } }
Then we can construct with our new DisabledScrollTextView.scrollableTextView
Use NSMutableAttributedString and add attribute for whole range
1 2 3 4
let a: NSAttributedString let m: NSMutableAttributedString = NSMutableAttributedString(attributedString: a) let range = NSRange(location: 0, length: a.length) m.addAttribute(.backgroundColor, value: NSColor.clear, range: range)
When you want to check for existence using Bool, consider using Set over Dictionary with Bool, as Set guarantee uniqueness. If using Dictionary instead, the value for key is Optional<Bool> where we have to check for both optional and true false within.
NSStatusItem is backed by NSButton, we can animate this inner button. We need to specify position and anchorPoint for button’s layer so it rotates around its center point
1 2 3 4 5 6 7 8 9 10 11 12
guard let button = statusItem.button else { return }
Use NSSharingService.sharingServices(forItems:) with an array of one empty string gives a list of sharing items. There we show image and title of each menu item.
We should cache sharing items as that can cause performance issue
Below is an example of a parent ContentView with State and a child Sidebar with a Binding.
The didSet is only called for the property that is changed.
When we click Button in ContentView, that changes State property, so only the didSet in ContentView is called When we click Button in Sidebar, that changes Binding property, so only the didSet in Sidebar is called
var body: some View { Sidebar(tag: $tag) Button(action: { tag = .settings }) { Text("Button in ContentView") } } }
structSidebar: View{ @Binding var tag: Tag { didSet { print("Sidebar \(tag)") } }
var body: some View { Text(tag.rawValue) Button(action: { tag = .settings }) { Text("Button in Sidebar") } } }
Custom Binding with get set
Another way to observe Binding changes is to use custom Binding with get, set. Here even if we click Button in ContentView, the set block is triggered and here we can change State
1 2 3 4 5 6 7 8 9 10 11 12
var body: some View { Sidebar(tag: Binding<Tag>( get: { tag }, set: { newTag in self.tag = newTag print("ContentView newTag \(newTag)") } )) Button(action: { tag = .settings }) { Text("Button in ContentView") } }
Convenient new Binding
We can also make convenient extension on Binding to return new Binding, with a hook allowing us to inspect newValue. So we can call like Sidebar(tag: $tag.didSet
Describe all possible paths in your program with enum. This is great to track down bugs and to not miss representing potential cases in UI. Errors can come from app layer, backend layer to network issues.
Enum is handy in both UIKit and SwiftUI
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
enumNetworkError: Swift.Error{ enumRequestError{ case invalidRequest(URLRequest) case encodingError(Swift.EncodingError) case other(NSError) }
enumServerError{ case decodingError(Swift.DecodingError) case noInternetConnection case timeout case internalServerError case other(statusCode: Int, response: HTTPURLResponse) }
case requestError(RequestError) case serverError(ServerError) }
and note that Foundation has NSError with a bunch of well defined error code ready to use. We can use this before introduce our custom error cases
publicvarNSURLErrorUnknown: Int { get } publicvarNSURLErrorCancelled: Int { get } publicvarNSURLErrorBadURL: Int { get } publicvarNSURLErrorTimedOut: Int { get } publicvarNSURLErrorUnsupportedURL: Int { get } publicvarNSURLErrorCannotFindHost: Int { get } publicvarNSURLErrorCannotConnectToHost: Int { get } publicvarNSURLErrorNetworkConnectionLost: Int { get } publicvarNSURLErrorDNSLookupFailed: Int { get } publicvarNSURLErrorHTTPTooManyRedirects: Int { get } publicvarNSURLErrorResourceUnavailable: Int { get } publicvarNSURLErrorNotConnectedToInternet: Int { get } publicvarNSURLErrorRedirectToNonExistentLocation: Int { get } publicvarNSURLErrorBadServerResponse: Int { get } publicvarNSURLErrorUserCancelledAuthentication: Int { get } publicvarNSURLErrorUserAuthenticationRequired: Int { get } publicvarNSURLErrorZeroByteResource: Int { get } publicvarNSURLErrorCannotDecodeRawData: Int { get } publicvarNSURLErrorCannotDecodeContentData: Int { get } publicvarNSURLErrorCannotParseResponse: Int { get }
List has a selection parameter where we can pass selection binding. As we can see here selection is of type optional Binding<Set<SelectionValue>>? where SelectionValue is any thing conforming to Hasable
The best way to test is to not have to mock at all. The second best way is to have your own abstraction over the things you would like to test, either it is in form of protocol or some function injection.
But in case you want a quick way to test things, and want to test as real as possible, then for some cases we can be creative to mock the real objects.
One practical example is when we have some logic to handle notification, either showing or deep link user to certain screen. From iOS 10, notifications are to be delivered via UNUserNotificationCenterDelegate
// The notification to which the user responded. @NSCopyingopenvar notification: UNNotification { get }
That class inherits from NSCopying which means it is constructed from NSCoder, but how do we init it?
1
let response = UNNotificationResponse(coder: ???)
NSObject and NSCoder
The trick is, since UNNotificationResponse is NSObject subclass, it is key value compliant, and since it is also NSCopying compliant, we can make a mock coder to construct it
On iOS 12, we need to add decodeInt64 method, otherwise UNNotificationResponse init fails. This is not needed on iOS 14
UNNotificationResponse has a read only UNNotification, which has a readonly UNNotificationRequest, which can be constructed from a UNNotificationContent
Luckily UNNotificationContent has a counterpart UNMutableNotificationContent
Now we can make a simple extension on UNNotificationResponse to quickly create that object in tests
Another way is to build a proper KeyedArchiver that checks key and return correct property. Note that we can reuse the same NSKeyedArchiver to nested properties.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
privatefinalclassKeyedArchiver: NSKeyedArchiver{ let request: UNNotificationRequest let actionIdentifier: String let notification: UNNotification