How to add alternative app icons for iOS

Issue #749

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

Here’s how it is done on my app PastePal

Screenshot 2021-01-14 at 06 46 40

Add app icons to project folder

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.

Declare in Info.plist

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<key>CFBundleIcons</key>
<dict>
<key>CFBundlePrimaryIcon</key>
<dict>
<key>CFBundleIconFiles</key>
<array>
<string>AppIcon60x60</string>
</array>
<key>UIPrerenderedIcon</key>
<false/>
</dict>
<key>CFBundleAlternateIcons</key>
<dict>
<key>AppIconPride1</key>
<dict>
<key>CFBundleIconFiles</key>
<array>
<string>AppIconPride1</string>
</array>
<key>UIPrerenderedIcon</key>
<false/>
</dict>
<key>AppIconPride2</key>
<dict>
<key>CFBundleIconFiles</key>
<array>
<string>AppIconPride2</string>
</array>
<key>UIPrerenderedIcon</key>
<false/>
</dict>
</dict>
</dict>

Asset Catalog

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
enum AlternateAppIcon: String, CaseIterable, Identifiable {
var id: AlternateAppIcon { self }

case main = "Main"
case pride1 = "Pride1"
case pride2 = "Pride2"
}

func onChange(_ icon: AlternateAppIcon) {
guard UIApplication.shared.supportsAlternateIcons else { return }
switch icon {
case .main:
UIApplication.shared.setAlternateIconName(nil)
default:
UIApplication.shared.setAlternateIconName(icon.name)
}
}

Change app icon for iPad

So far we’re only talk about iPhone. To support iPad, we need to specify iPad versions in Info.plist with CFBundleIcons~ipad

1
2
3
4
5
6
7
8
- CFBundleIcons
- CFBundleAlternateIcons
- Icon Name
- CFBundleIconFiles
- CFBundleIcons~ipad
- CFBundleAlternateIcons
- Icon Name
- CFBundleIconFiles

Also notice the size for icons on iPad

Screenshot 2021-01-14 at 07 01 23

Updated at 2021-01-14 06:02:28

How to make popup button in SwiftUI for macOS

Issue #748

There is said to be PopUpButtonPickerStyle and MenuPickerStyle but these don’t seem to work.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
Menu {
Button("About", action: ActionService.onAbout)
Button("Quit", action: ActionService.onQuit)
} label: {
Text("")
}
.frame(width: 24)
.opacity(0.01)
.overlay(
makeButton(action: {}, "gearshape.fill")
.disabled(true)
.foregroundColor(Color.secondaryLabel)
)

Follow pika

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
struct ColorMenu: View {
var eyedropper: Eyedropper

var body: some View {
if #available(OSX 11.0, *) {
Menu {
ColorMenuItems(eyedropper: eyedropper)
} label: {
Image(systemName: "ellipsis.circle")
}
.menuStyle(BorderlessButtonMenuStyle(showsMenuIndicator: false))
} else {
MenuButton(label: IconImage(name: "ellipsis.circle"), content: {
ColorMenuItems(eyedropper: eyedropper)
})
.menuButtonStyle(BorderlessButtonMenuButtonStyle())
}
}
}

struct ColorMenuItems: View {
var eyedropper: Eyedropper
let pasteboard = NSPasteboard.general

var body: some View {
VStack(alignment: .leading, spacing: 0.0) {
Text(eyedropper.title)
Divider()
}
Button(action: {
pasteboard.clearContents()
pasteboard.setString(eyedropper.color.toHex, forType: .string)
}, label: { Text("Copy color hex") })
Button(action: {
pasteboard.clearContents()
pasteboard.setString(eyedropper.color.toRGB, forType: .string)
}, label: { Text("Copy RGB values") })
Button(action: {
pasteboard.clearContents()
pasteboard.setString(eyedropper.color.toHSB, forType: .string)
}, label: { Text("Copy HSB values") })
}
}

Need to specify .fixedSize() for menu rows to hug content. Can also use opacity to reduce Menu button color

1
2
3
Menu
.fixedSize()
.opacity(0.8)

Updated at 2021-01-21 09:12:48

How to use UITextView in SwiftUI

Issue #747

Need to use Coordinator conforming to UITextViewDelegate to apply changes back to Binding

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import SwiftUI
import UIKit

struct MyTextView: UIViewRepresentable {
@Binding
var text: String

final class Coordinator: NSObject, UITextViewDelegate {
let parent: MyTextView

init(parent: MyTextView) {
self.parent = parent
}

func textViewDidChange(_ textView: UITextView) {
if textView.text != parent.text {
parent.text = textView.text
}
}
}

func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}

func makeUIView(context: Context) -> UITextView {
let view = UITextView()
view.isScrollEnabled = true
view.isEditable = true
view.isUserInteractionEnabled = true
view.font = UIFont.preferredFont(forTextStyle: .body)
view.delegate = context.coordinator
return view
}

func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
}
}

How to check app going to background in SwiftUI

Issue #746

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

1
2
3
4
5
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
</dict>

One way to be notified about application life cycle is to use UIApplicationDelegateAdaptor and via NotificationCenter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import SwiftUI
import UIKit
import FontAwesomeSwiftUI

final class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
FontAwesome.register()
PreferenceManager.shared.load()
return true
}
}

@main
struct MyAwesomeApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self)
var appDelegate

var body: some Scene {
WindowGroup {
MainView(store: Store.shared)
.onReceive(
NotificationCenter.default.publisher(
for: UIApplication.didEnterBackgroundNotification)) { _ in
PreferenceManager.shared.save()
}
}
}
}

How to use selection in List in SwiftUI

Issue #745

I used to use selection with Binding where wrappedValue is optional, together with tag in SwiftUI for macOS, and it shows current selection

1
2
3
4
5
6
7
@Binding
var selection: Tag? = .all

List(section: $selection) {
Text("All")
.tag(Tag.all)
}

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>>?

1
init<Data, ID, RowContent>(Data, id: KeyPath<Data.Element, ID>, selection: Binding<Set<SelectionValue>>?, rowContent: (Data.Element) -> RowContent)

So let’s use Set. It shows current selection and I don’t need to use .tag also

let selection: Binding<Set<SidebarTag>> = Binding<Set<SidebarTag>>(
    get: { Set(arrayLiteral: store.sidebarTag) },
    set: { newValue in
        DispatchQueue.main.async {
            if let first = newValue.first {
                store.sidebarTag = first
            }
        }
    }
)

List(selection: selection) {
    Text("All")
}

Updated at 2021-01-06 21:13:43

How to simplify communication patterns with closure in Swift

Issue #744

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.

Table of Contents

The many communication patterns

In iOS, we can handle button tap by using addTarget which is a function that any UIControl has:

let button = UIButton()
button.addTarget(self, action: #selector(buttonTouched(_:)), for: .touchUpInside)

@objc func buttonTouched(_ sender: UIButton) {}

In macOS, due to historical reasons, the syntax is a bit different:

button.target = self
button.action = #selector(buttonTouched(_:))

@objc func buttonTouched(_ sender: NSButton) {}

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.

[https://www.objc.io/issues/7-foundation/communication-patterns/](https://www.objc.io/issues/7-foundation/communication-patterns/)https://www.objc.io/issues/7-foundation/communication-patterns/

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:

var viewController: UIViewController? = UIViewController()
viewController?.notifier = Notifier()
viewController = nil
XCTAssertNil(viewController?.notifier)

Confusing code completion

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.

class On {
    func tap(_ closure: () -> Void) {}
    func tapRow(_ closure: () -> Void) {}
    func textChange(_ closure: () -> Void) {}
}

to NSObject:

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:

public extension Container where Host: UIButton {
    func tap(_ action: [@escaping](http://twitter.com/escaping) Action) {
        let target = ButtonTarget(host: host, action: action)
        targets[ButtonTarget.uniqueId] = target
    }
}

class ButtonTarget: NSObject {
    var action: Action?

init(host: UIButton, action: [@escaping](http://twitter.com/escaping) Action) {
        super.init()

self.action = action
        host.addTarget(self, action: #selector(handleTap), for: .touchUpInside)
    }

// MARK: - Action

[@objc](http://twitter.com/objc) func handleTap() {
        action?()
    }
}

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:

public extension Container where Host: UITextField {
    func textChange(_ action: [@escaping](http://twitter.com/escaping) StringAction) {
        let target = TextFieldTarget(host: host, textAction: action)
        targets[TextFieldTarget.uniqueId] = target
    }
}

class TextFieldTarget: NSObject {
    var textAction: StringAction?

required init(host: UITextField, textAction: [@escaping](http://twitter.com/escaping) StringAction) {
        super.init()

self.textAction = textAction
        host.addTarget(self, action: #selector(handleTextChange(_:)), for: .editingChanged)
    }

// MARK: - Action

[@objc](http://twitter.com/objc) func handleTextChange(_ textField: UITextField) {
        textAction?(textField.text ?? "")
    }
}

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.

This is what we did before iOS 10:

func schedule() {
    DispatchQueue.main.async {
      self.timer = Timer.scheduledTimer(timeInterval: 20, target: self,
                                   selector: #selector(self.timerDidFire(timer:)), userInfo: nil, repeats: false)
    }
}

[@objc](http://twitter.com/objc) private func timerDidFire(timer: Timer) {
    print(timer)
}

We can easily extend Timer with our on property by introducing tick method:

public extension Container where Host: Timer {

func tick(_ action: [@escaping](http://twitter.com/escaping) Action) {
        self.timerTarget?.action = action
    }
}

class TimerTarget: NSObject {
    var action: Action?

[@objc](http://twitter.com/objc) func didFire() {
        action?()
    }
}

public extension Timer {
    static func scheduledTimer(withTimeInterval interval: TimeInterval, repeats: Bool) -> Timer {
        let target = TimerTarget()
        let timer = Timer.scheduledTimer(timeInterval: interval,
                                         target: target,
                                         selector: #selector(TimerTarget.didFire),
                                         userInfo: nil,
                                         repeats: repeats)
        timer.on.timerTarget = target
        return timer
    }
}

So we can use with timer.on.tick:

timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true)
timer.on.tick { print("tick") }

But as of iOS 10, Timer gets its closure based API, so now we can just call the static method scheduledTimer:

DispatchQueue.main.async {
      self.timer = Timer.scheduledTimer(withTimeInterval: 20, repeats: false) { timer in
        print(timer)
      }
}

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)
  }
}

Read more

How to make Auto Layout more convenient in iOS

Issue #742

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ViewController: UIViewController {

let box = UIView()
override func viewDidLoad() {
super.viewDidLoad()

box.backgroundColor = UIColor.red
view.addSubview(box)
}

override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()

box.frame = CGRect(x: 20, y: 50, width: view.frame.size.width - 40, height: 100)
}
}

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.

box.autoresizingMask = [.flexibleWidth, .flexibleBottomMargin]

Here are a few more scenarios

  • 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:

item1.attribute1 = multiplier × item2.attribute2 + constant

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.

1
2
3
4
5
let marginTop = NSLayoutConstraint(item: box, attribute: .top, relatedBy: .equal, toItem: view, attribute: .top, multiplier: 1.0, constant: 50)
let marginLeft = NSLayoutConstraint(item: box, attribute: .left, relatedBy: .equal, toItem: view, attribute: .left, multiplier: 1.0, constant: 20)
let marginRight = NSLayoutConstraint(item: box, attribute: .right, relatedBy: .equal, toItem: view, attribute: .right, multiplier: 1.0, constant: -20)
let height = NSLayoutConstraint(item: box, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 100)
NSLayoutConstraint.activate([marginTop, marginLeft, marginRight, height])

translatesAutoresizingMaskIntoConstraints

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

box.translatesAutoresizingMaskIntoConstraints = false

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.

Here is how to achieve that same red box

NSLayoutConstraint.activate([
    box.topAnchor.constraint(equalTo: view.topAnchor, constant: 50),
    box.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20),
    box.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20),
    box.heightAnchor.constraint(equalToConstant: 100)
])

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.

box.topAnchor.constraint(equalTo: view.leftAnchor, constant: 50),

Ambiguous error message with NSLayoutAnchor

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.

NSLayoutConstraint.activate([
    box.topAnchor.constraint(equalTo: view.centerXAnchor, constant: 50)
])

Xcode complains of unwrapped UIView problem which may confuse us even more.

Value of optional type 'UIView?' must be unwrapped to refer to member 'centerXAnchor' of wrapped base type 'UIView'

Another puzzle, regarding this code

NSLayoutConstraint.activate([
    imageView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
    imageView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 8),
    imageView.heightAnchor.constraint(equalToConstant: view.heightAnchor, mult0.7),
    imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor, multiplier: 1.0)
])

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:

constrain(button1, button2) { button1, button2 in
    button1.right == button2.left - 12
}

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

constrain(logoImnageView, view1, view2) { logoImageView, view1, view2 in
    logoImageView.with == 74
    view1.left == view2.left + 20
}

to simple NSLayoutAnchor

Constraint.on(
  logoImageView.widthAnchor.constraint(equalToConstant: 74),
  view1.leftAnchor.constraint(equalTo: view2.leftAnchor, constant: 20),
)

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.

[https://github.com/onmyway133/blog/issues/22](https://github.com/onmyway133/blog/issues/22)https://github.com/onmyway133/blog/issues/22

keyB.snp.makeConstraints { (make) -> Void in
   make.left.equalTo(self.keyA.right)
}

keyC.snp.makeConstraints { (make) -> Void in
   make.left.equalTo(self.keyB.right)
}

keyD.snp.makeConstraints { (make) -> Void in
   make.left.equalTo(self.keyC.right)
}

The many overloading functions

There are also attempts to build simple Auto Layout wrapper functions but that escalates very quickly.

We might begin with an extension that pins edge constraints to superview.

box.pinEdgesToSuperview()

But a view does not always pin to its superview, it can be to another view, then we add another function

box.pinEdgesToView(_ view: UIView)

It would be nice if there is some padding, isn’t it? Let’s add insets options

box.pinEdgesToView(_ view: UIView, insets: UIEdgeInsets)

There might be cases where we only want to pin top, left, and right and not the bottom, let’s add another parameter

box.pinEdgesToView(_ view: UIView, insets: UIEdgeInsets, exclude: NSLayoutConstraint.Attribute)

Constraints are not always 1000 priorities, it can be lower. We need to support that

box.pinEdgesToView(_ view: UIView, insets: UIEdgeInsets, exclude: NSLayoutConstraint.Attribute, priority: NSLayoutConstraint.Priority)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public extension NSLayoutConstraint {

/// Disable auto resizing mask and activate constraints
///
/// - Parameter constraints: constraints to activate
static func on(_ constraints: [NSLayoutConstraint]) {
constraints.forEach {
($0.firstItem as? UIView)?.translatesAutoresizingMaskIntoConstraints = false
$0.isActive = true
}
}

static func on(_ constraintsArray: [[NSLayoutConstraint]]) {
let constraints = constraintsArray.flatMap({ $0 })
NSLayoutConstraint.on(constraints)
}
}

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public extension UIView {
func pinCenter(view: UIView) -> [NSLayoutConstraint] {
return [
centerXAnchor.constraint(equalTo: view.centerXAnchor),
centerYAnchor.constraint(equalTo: view.centerYAnchor)
]
}

func pin(size: CGSize) -> [NSLayoutConstraint] {
return [
widthAnchor.constraint(equalToConstant: size.width),
heightAnchor.constraint(equalToConstant: size.height)
]
}
}

With that, we can pin the red box with a certain size and to the center of the screen:

NSLayoutConstraint.on([
    box.pinCenter(view: view),
    box.pin(size: CGSize(width: 100, height: 50))
])

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 😢

let topConstraint = box.topAnchor.constraint(equalTo: view.topAnchor, constant: 50)
topConstraint.priority = UILayoutPriority.defaultLow

NSLayoutConstraint.on([
    topConstraint
])

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:

activate(
  boxA.anchor.top.left,
  boxB.anchor.top.right,
  boxC.anchor.bottom.left,
  boxD.anchor.bottom.right
)

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
enum To {
case anchor(Anchor)
case size
case none
}
class Pin {
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:

func paddingHorizontally(_ value: CGFloat) -> Anchor {
  removeIfAny(.leading)
  removeIfAny(.trailing
  pins.append(Pin(.leading, constant: value))
  pins.append(Pin(.trailing, constant: -value)
  return self
}

Inferring constraints

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.

box.anchor.width.constant(10)
box.anchor.height.ratio(2) // height==width*2

This is easily achieved by checking our pins array

if sourceAnchor.exists(.width) {
  return Anchor(item: sourceAnchor.item).width
    .equal
    .to(Anchor(item: sourceAnchor.item).height)
    .multiplier(ratio).constraints()
} else if sourceAnchor.exists(.height) {
  return Anchor(item: sourceAnchor.item).height
    .equal
    .to(Anchor(item: sourceAnchor.item).width)
    .multiplier(ratio).constraints()
} else {
  return []
}

Retrieving a constraint

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public extension Anchor {
/// Find a constraint based on an attribute
func find(_ attribute: Attribute) -> NSLayoutConstraint? {
guard let view = item as? View else {
return nil
}
var constraints = view.superview?.constraints
if attribute == .width || attribute == .height {
constraints?.append(contentsOf: view.constraints)
}
return constraints?.filter({
guard $0.firstAttribute == attribute else {
return false
}
guard $0.firstItem as? NSObject == view else {
return false
}
return true
}).first
}
}

How to reset constraints

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.

constrain(view, replace: group) { view in
    view.top  == view.superview!.top
    view.left == view.superview!.left
}

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 😉

[https://github.com/onmyway133/Anchors](https://github.com/onmyway133/Anchors)https://github.com/onmyway133/Anchors

activate(
  lineBlock.anchor.left.bottom
)

// later
activate(
  firstSquareBlock.anchor.left.equal.to(lineBlock.anchor.right),
  firstSquareBlock.anchor.bottom
)

// later
activate(
  secondSquareBlock.anchor.right.bottom
)

Read more


Updated at 2021-01-05 07:22:31

How to make tiled image in SwiftUI

Issue #737

Use resizingMode of .tile with a tile image from https://www.transparenttextures.com/

1
2
3
4
5
6
Image("transparentTile")
.resizable(capInsets: .init(), resizingMode: .tile)
.scaleEffect(2)
.aspectRatio(contentMode: .fit)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.clipped()

Updated at 2021-01-02 22:47:57

How to use WebView in SwiftUI

Issue #736

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
struct MyWebView: NSViewRepresentable {
let url: URL
@Binding
var isLoading: Bool

func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}

func makeNSView(context: Context) -> WKWebView {
let view = WKWebView()
view.navigationDelegate = context.coordinator
view.load(URLRequest(url: url))
return view
}

func updateNSView(_ nsView: WKWebView, context: Context) {

}

class Coordinator: NSObject, WKNavigationDelegate {
let parent: MyWebView

init(parent: MyWebView) {
self.parent = parent
}

func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
parent.isLoading = true
}

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
parent.isLoading = false
}
}
}

How to use GeometryReader in SwiftUI

Issue #735

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
VStack {
Rectangle()
.fill(Color.gray)
.overlay(
GeometryReader { geo in
Text("\(Int(geo.size.width))x\(Int(geo.size.height))")
.bold()
}
)
}
.frame(width: 300, height: 300)
}
}
Screenshot 2021-01-02 at 00 37 17

To align content center, we can specify frame with geo information

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
VStack {
Rectangle()
.fill(Color.gray)
.overlay(
GeometryReader { geo in
Text("\(Int(geo.size.width))x\(Int(geo.size.height))")
.bold()
.frame(width: geo.size.width, height: geo.size.height, alignment: .center)
}
)
}
.frame(width: 300, height: 300)
}
}

The result is that Text is center aligned

Screenshot 2021-01-02 at 00 39 13

If we were to implement GeometryReader, it would look like this

1
2
3
4
5
6
7
8
9
10
11
12
13
struct GeometryReader<Content: View>: View {
let content: (CGSize) -> Content

func size(proposedSize: CGSize) -> CGSize {
// Take up whole size proposed by parent
proposedSize
}

func buildBody(calculatedSize: CGSize) -> some View {
// Pass in the calculated size
content(calculatedSize)
}
}

Updated at 2021-01-01 23:42:34

How to use flexible frame in SwiftUI

Issue #734

In SwiftUI there are fixed frame and flexible frame modifiers.

Fixed frame Positions this view within an invisible frame with the specified size.

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.

1
2
3
4
5
6
7
8
VStack {
Ellipse()
.fill(Color.purple)
.frame(width: 200, height: 100)
Ellipse()
.fill(Color.blue)
.frame(height: 100)
}

Flexible frame frame(minWidth:idealWidth:maxWidth:minHeight:idealHeight:maxHeight:alignment:)

Read the documentation carefully

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
import SwiftUI

struct Examine: View {
let parentWidth: CGFloat
let contentWidth: CGFloat
var minWidth: CGFloat?
var maxWidth: CGFloat?

var body: some View {
Rectangle()
.fill(Color.gray)
.border(Color.black, width: 3)
.frame(width: contentWidth)
.overlay(
GeometryReader { geo in
Text("\(geo.size.width)")
.foregroundColor(Color.blue)
.offset(x: 0, y: -20)
.center()
}
)
.border(Color.blue, width: 2)
.frame(minWidth: minWidth, maxWidth: maxWidth)
.overlay(
GeometryReader { geo in
Text("\(geo.size.width)")
.foregroundColor(Color.red)
.center()
}
)
.border(Color.red, width: 1)
.frame(width: parentWidth, height: 100)
}
}

extension View {
func center() -> some View {
VStack {
Spacer()
HStack {
Spacer()
self
Spacer()
}
Spacer()
}
}
}

struct Examine_Previews: PreviewProvider {
static var previews: some View {
Group {
Group {
Text("Both minWidth and maxWidth")
Examine(parentWidth: 75, contentWidth: 150, minWidth: 100, maxWidth: 200)
.help("proposed size < min width")
Examine(parentWidth: 125, contentWidth: 150, minWidth: 100, maxWidth: 200)
.help("min width < proposed size < content")
Examine(parentWidth: 175, contentWidth: 150, minWidth: 100, maxWidth: 200)
.help("min width < content < proposed size")
Examine(parentWidth: 300, contentWidth: 150, minWidth: 100, maxWidth: 200)
.help("min width < content < max width < proposed size")
}

Group {
Text("Just minWidth")
Examine(parentWidth: 75, contentWidth: 150, minWidth: 100)
Examine(parentWidth: 125, contentWidth: 150, minWidth: 100)
Examine(parentWidth: 175, contentWidth: 150, minWidth: 100)
Examine(parentWidth: 175, contentWidth: 75, minWidth: 100)
.help("content < minWidth")
}

Group {
Text("Just maxWidth")
Examine(parentWidth: 75, contentWidth: 150, maxWidth: 200)
Examine(parentWidth: 125, contentWidth: 150, maxWidth: 200)
Examine(parentWidth: 175, contentWidth: 150, maxWidth: 200)
Examine(parentWidth: 300, contentWidth: 225, maxWidth: 200)
.help("content > maxWidth")
}
}
}
}

Observation

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)
Screenshot 2021-01-01 at 23 30 19

🍅 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)
Screenshot 2021-01-01 at 23 51 46

🍏 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)
Screenshot 2021-01-01 at 23 52 27

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

To understand this, let’s read 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)
Screenshot 2021-01-02 at 00 15 14

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)
Screenshot 2021-01-02 at 00 16 11

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
struct Text_Previews: PreviewProvider {
static var 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

Screenshot 2021-01-02 at 00 17 29

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
VStack(spacing: 16) {
Rectangle()
.fill(Color.red)
.fixedSize()

Rectangle()
.fill(Color.green)
.frame(width: 100, height: 100)

Rectangle()
.fill(Color.blue)
.frame(idealWidth: 200, idealHeight: 200)
.fixedSize(horizontal: true, vertical: true)
.frame(height: 50)
.border(Color.gray)
}
.padding()
.frame(width: 500, height: 500)

}
}
Screenshot 2021-01-02 at 00 23 29

Updated at 2021-01-01 23:25:08

How to disable scrolling in NSTextView for macOS

Issue #733

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
class DisabledScrollTextView: NSTextView {
override func scrollWheel(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

Updated at 2020-12-31 07:45:19

How to override attribute string in Swift

Issue #732

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)

How to make view appear with delay in SwiftUI

Issue #731

Sometimes we don’t want to show progress view right away

1
2
3
4
5
6
HUDProgressView()
.transition(
AnyTransition.asymmetric(
insertion: AnyTransition.opacity.animation(Animation.default.delay(1)),
removal: AnyTransition.opacity)
)

Updated at 2020-12-31 05:33:00

How to make attributed string Text in SwiftUI for macOS

Issue #730

Use NSTextField with maximumNumberOfLines

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import AppKit
import SwiftUI

struct AttributedText: NSViewRepresentable {

let attributedString: NSAttributedString

init(_ attributedString: NSAttributedString) {
self.attributedString = attributedString
}

func makeNSView(context: Context) -> NSTextField {
let textField = NSTextField()

textField.lineBreakMode = .byClipping
textField.maximumNumberOfLines = 0
textField.isBordered = false

return textField
}

func updateNSView(_ nsView: NSTextField, context: Context) {
nsView.attributedStringValue = attributedString
}
}

TextField has problem with wrapping, we can use TextView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
struct AttributedTextView: NSViewRepresentable {
typealias NSViewType = NSScrollView

let attributedText: NSAttributedString?
let isSelectable: Bool
var insetSize: CGSize = .zero

func makeNSView(context: Context) -> NSViewType {
let scrollView = NSTextView.scrollableTextView()

let textView = scrollView.documentView as! NSTextView
textView.drawsBackground = false
textView.textColor = .controlTextColor
textView.textContainerInset = insetSize

return scrollView
}

func updateNSView(_ nsView: NSViewType, context: Context) {
let textView = (nsView.documentView as! NSTextView)
textView.isSelectable = isSelectable

if let attributedText = attributedText,
attributedText != textView.attributedString() {
textView.textStorage?.setAttributedString(attributedText)
}

if let lineLimit = context.environment.lineLimit {
textView.textContainer?.maximumNumberOfLines = lineLimit
}
}
}

Updated at 2020-12-31 05:51:42

How to do copy paste delete in Swift for macOS

Issue #729

1
2
3
4
5
6
7
8
9
10
11
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
@IBAction func copy(_ sender: Any) {
print("copy", sender)
}


@IBAction func paste(_ sender: Any) {
print("paste", sender)
}
}

For delete, we can listen to keyDown in NSWindow

1
2
3
4
5
6
7
8
9
10
11
12
class MyWindow: NSWindow {
override func keyDown(with event: NSEvent) {
super.keyDown(with: event)

guard
let deleteScalar = UnicodeScalar(NSDeleteCharacter),
event.charactersIgnoringModifiers == String(deleteScalar)
else { return }

NotificationCenter.default.post(Notification(name: .didKeyboardDeleteItem))
}
}

Updated at 2020-12-30 05:48:29

How to make simple NSItemProvider in Swift

Issue #728

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
NSItemProvider(object: StringProvider(string: string))

class StringProvider: NSObject, NSItemProviderWriting {
let string: String
init(string: String) {
self.string = string
super.init()
}

static var writableTypeIdentifiersForItemProvider: [String] {
return [(kUTTypeData) as String]
}

func loadData(
withTypeIdentifier typeIdentifier: String,
forItemProviderCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void
) -> Progress? {
let data = string.data(using: .utf8)
completionHandler(data, nil)
return Progress(totalUnitCount: 100)
}
}

How to make UserDefaults property wrapper

Issue #726

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@propertyWrapper
struct UserDefault<Value> {
let key: String
let defaultValue: Value
let container: UserDefaults = .standard

var wrappedValue: Value {
get {
return container.object(forKey: key) as? Value ?? defaultValue
}
set {
container.set(newValue, forKey: key)
}
}
}

Then we can use it as property and provide default value

1
2
3
final class KeepHistoryService {
@UserDefault(key: "keepHistoryCheckDate", defaultValue: nil)
var checkDate: Date?

How to use Set to check for bool in Swift

Issue #725

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct Book: Hashable {
let id: UUID
let name: String
}

let book1 = Book(id: UUID(), name: "1")
let book2 = Book(id: UUID(), name: "2")

func useDictionary() {
var hasChecked: [Book: Bool] = [:]
hasChecked[book1] = true
print(hasChecked[book1] == Optional<Bool>(true))
print(hasChecked[book2] == Optional<Bool>.none)
}

func useSet() {
var hasChecked: Set<Book> = Set()
hasChecked.insert(book1)
print(hasChecked.contains(book1))
print(hasChecked.contains(book2))
}

How to make visual effect blur in SwiftUI for macOS

Issue #724

We can use .blur modifier, but with VisualEffectView gives us more options for material and blending mode.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public struct VisualEffectView: NSViewRepresentable {
let material: NSVisualEffectView.Material
let blendingMode: NSVisualEffectView.BlendingMode

public init(
material: NSVisualEffectView.Material = .contentBackground,
blendingMode: NSVisualEffectView.BlendingMode = .withinWindow
) {
self.material = material
self.blendingMode = blendingMode
}

public func makeNSView(context: Context) -> NSVisualEffectView {
let visualEffectView = NSVisualEffectView()
visualEffectView.material = material
visualEffectView.blendingMode = blendingMode
visualEffectView.state = NSVisualEffectView.State.active
return visualEffectView
}

public func updateNSView(_ visualEffectView: NSVisualEffectView, context: Context) {
visualEffectView.material = material
visualEffectView.blendingMode = blendingMode
}
}

How to make simple HUD in SwiftUI

Issue #723

Use @ViewBuilder to build dynamic content for our HUD. For blur effect, here I use NSVisualEffectView, but we can use .blur modifier also

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct HUD<Content>: View where Content: View {
let content: () -> Content

init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
}

var body: some View {
content()
.frame(width: 80, height: 80)
.background(
VisualEffectView(material: .hudWindow)
.clipShape(RoundedRectangle(cornerRadius: 12))
.shadow(color: Color.black.opacity(0.22), radius: 12, x: 0, y: 5)
)
}
}

Then we can make some wrappers for information and progress HUD

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct HUD<Content>: View where Content: View {
let content: () -> Content

init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
}

var body: some View {
content()
.frame(width: 80, height: 80)
.background(
VisualEffectView()
)
.cornerRadius(10)
.shadow(color: Color.gray.opacity(0.3), radius: 1, x: 0, y: 1)
}
}

Updated at 2020-12-26 20:59:10

How to force set frame explicitly for NSWindow

Issue #721

For setFrame to take effect

1
mainWindow.setFrame(rect, display: true)

we can remove auto save frame flag

1
mainWindow.setFrameAutosaveName("MyApp.MainWindow")

How to rotate NSStatusItem

Issue #720

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 }

let animation = CABasicAnimation(keyPath: "transform.rotation.z")
animation.fromValue = 0
animation.toValue = CGFloat.pi * 2
animation.duration = 0.25
animation.repeatCount = 1
button.layer?.position = NSPoint(x: NSMidX(button.frame), y: NSMidY(button.frame))
button.layer?.anchorPoint = NSPoint(x: 0.5, y: 0.5)
button.layer?.add(animation, forKey: "rotate")

Updated at 2020-12-26 21:21:00

How to make sharing menu in SwiftUI for macOS

Issue #718

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import SwiftUI
import AppKit
import EasySwiftUI

extension NSSharingService {
private static let items = NSSharingService.sharingServices(forItems: [""])
static func submenu(text: String) -> some View {
return Menu(
content: {
ForEach(items, id: \.title) { item in
Button(action: { item.perform(withItems: [string]) }) {
Image(nsImage: item.image)
Text(item.title)
}
}
},
label: {
Text("Share")
Image(systemName: SFSymbol.squareAndArrowUp.rawValue)
}
)
}
}

Alternative, you can trigger NSSharingServicePicker from a button, it shows a context menu with sharing options

Read more


Updated at 2020-12-25 22:57:57

How to do didSet for State and Binding in SwiftUI

Issue #714

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
enum Tag: String {
case all
case settings
}

struct ContentView: View {
@State
private var tag: Tag = .all {
didSet {
print("ContentView \(tag)")
}
}

var body: some View {
Sidebar(tag: $tag)
Button(action: { tag = .settings }) {
Text("Button in ContentView")
}
}
}

struct Sidebar: 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
extension Binding {
func didSet(_ didSet: @escaping (Value) -> Void) -> Binding<Value> {
Binding(
get: { wrappedValue },
set: { newValue in
self.wrappedValue = newValue
didSet(newValue)
}
)
}
}

struct ContentView: View {
@State
private var tag: Tag = .all {
didSet {
print("ContentView \(tag)")
}
}

var body: some View {
Sidebar(tag: $tag.didSet { newValue in
print("ContentView newTag \(newValue)")
})
Button(action: { tag = .settings }) {
Text("Button in ContentView")
}
}
}

Updated at 2021-02-06 21:57:28

How to add toolbar programatically in macOS

Issue #713

To setup toolbar, we need to implement NSToolbarDelegate that provides toolbar items. This delegate is responsible for many things

  • Set visible and allowed items with toolbarDefaultItemIdentifiers
  • Provide item with itemForItemIdentifier
  • Being notified with toolbarWillAddItem and toolbarDidRemoveItem
1
2
3
4
5
6
7
8
window.toolbarStyle = .unifiedCompact

let toolbar = NSToolbar(identifier: "Toolbar")
toolbar.displayMode = .iconAndLabel
toolbar.delegate = (NSApp.delegate as! AppDelegate)
toolbar.insertItem(withItemIdentifier: .add, at: 0)
toolbar.insertItem(withItemIdentifier: .settings, at: 1)
window.toolbar = toolbar
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
extension NSToolbarItem.Identifier {
static let add = NSToolbarItem.Identifier(rawValue: "Add")
static let settings = NSToolbarItem.Identifier(rawValue: "Settings")
}

extension AppDelegate: NSToolbarDelegate {
func toolbar(
_ toolbar: NSToolbar,
itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier,
willBeInsertedIntoToolbar flag: Bool
) -> NSToolbarItem? {
switch itemIdentifier {
case .add:
let item = NSToolbarItem(itemIdentifier: itemIdentifier)
item.label = "Add"
item.image = NSImage(named: NSImage.Name("add"))
let menuItem: NSMenuItem = NSMenuItem()
menuItem.submenu = nil
menuItem.title = "Add"
item.menuFormRepresentation = menuItem
item.toolTip = "Click here to add new entry"
return item
case .settings:
let item = NSToolbarItem(itemIdentifier: itemIdentifier)
item.label = "Settings"
let button = NSButton(image: NSImage(named: NSImage.Name("gear"))!, target: nil, action: nil)
button.bezelStyle = .texturedRounded
item.view = button
return item
default:
return nil
}
}

func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
[.add, .settings]
}

func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
[.add, .settings]
}
}

Read more

How to declare network Error with enum in Swift

Issue #712

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
enum NetworkError: Swift.Error {
enum RequestError {
case invalidRequest(URLRequest)
case encodingError(Swift.EncodingError)
case other(NSError)
}

enum ServerError {
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public var NSURLErrorUnknown: Int { get }
public var NSURLErrorCancelled: Int { get }
public var NSURLErrorBadURL: Int { get }
public var NSURLErrorTimedOut: Int { get }
public var NSURLErrorUnsupportedURL: Int { get }
public var NSURLErrorCannotFindHost: Int { get }
public var NSURLErrorCannotConnectToHost: Int { get }
public var NSURLErrorNetworkConnectionLost: Int { get }
public var NSURLErrorDNSLookupFailed: Int { get }
public var NSURLErrorHTTPTooManyRedirects: Int { get }
public var NSURLErrorResourceUnavailable: Int { get }
public var NSURLErrorNotConnectedToInternet: Int { get }
public var NSURLErrorRedirectToNonExistentLocation: Int { get }
public var NSURLErrorBadServerResponse: Int { get }
public var NSURLErrorUserCancelledAuthentication: Int { get }
public var NSURLErrorUserAuthenticationRequired: Int { get }
public var NSURLErrorZeroByteResource: Int { get }
public var NSURLErrorCannotDecodeRawData: Int { get }
public var NSURLErrorCannotDecodeContentData: Int { get }
public var NSURLErrorCannotParseResponse: Int { get }

How to programatically select row in List in SwiftUI

Issue #711

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

1
2
3
4
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public struct List<SelectionValue, Content> : View where SelectionValue : Hashable, Content : View {
@available(watchOS, unavailable)
public init(selection: Binding<Set<SelectionValue>>?, @ViewBuilder content: () -> Content)

So we can programatically control selection by tagging row with our own Tag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct SideView: View {
enum Tag: String, Hashable {
case all
case settings
}

@State
var selection: Tag? = .all

var body: some View {
List {
all
.tag(Tag.all)
categories
settings
.tag(Tag.settings)
}
}
}

How to mock UNNotificationResponse in unit tests

Issue #708

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

1
2
3
4
5
@available(iOS 10.0, *)
public protocol UNUserNotificationCenterDelegate : NSObjectProtocol {
@available(iOS 10.0, *)
optional func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void)
}

and all we get is UNNotificationResponse which has no real way to construct it.

1
2
3
4
5
6
@available(iOS 10.0, *)
open class UNNotificationResponse : NSObject, NSCopying, NSSecureCoding {


// The notification to which the user responded.
@NSCopying open var 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

1
2
3
4
private final class KeyedArchiver: NSKeyedArchiver {
override func decodeObject(forKey _: String) -> Any { "" }
override func decodeInt64(forKey key: String) -> Int64 { 0 }
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private extension UNNotificationResponse {
static func with(
userInfo: [AnyHashable: Any],
actionIdentifier: String = UNNotificationDefaultActionIdentifier
) throws -> UNNotificationResponse {
let content = UNMutableNotificationContent()
content.userInfo = userInfo
let request = UNNotificationRequest(
identifier: "",
content: content,
trigger: nil
)

let notification = try XCTUnwrap(UNNotification(coder: KeyedArchiver()))
notification.setValue(request, forKey: "request")

let response = try XCTUnwrap(UNNotificationResponse(coder: KeyedArchiver()))
response.setValue(notification, forKey: "notification")
response.setValue(actionIdentifier, forKey: "actionIdentifier")
return response
}
}

We can then test like normal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func testResponse() throws {
let data: [AnyHashable: Any] = [
"data": [
"type": "OPEN_ARTICLE",
"articleId": "1",
"articleType": "Fiction",
"articleTag": "1"
]
]
let response = try UNNotificationResponse.with(userInfo: data)
let centerDelegate = ArticleCenterDelegate()
centerDelegate.userNotificationCenter(
UNUserNotificationCenter.current(),
didReceive: response,
withCompletionHandler: {}
)
XCTAssertEqual(response.notification.request.content.userInfo["type"], "OPEN_ARTICLE")
XCTAssertEqual(centerDelegate.didOpenArticle, true)
}

decodeObject for key

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
private final class KeyedArchiver: NSKeyedArchiver {
let request: UNNotificationRequest
let actionIdentifier: String
let notification: UNNotification

override func decodeObject(forKey key: String) -> Any? {
switch key {
case "request":
return request
case "actionIdentifier":
return actionIdentifier
case "notification":
return UNNotification(coder: self)
default:
return nil
}
}
}

Updated at 2020-12-07 11:49:55