How to name Boolean property in Swift

Issue #787

In Swift, property, especially for Boolean flagging, uses the regular verb form for the third person. There are few exceptions, such as enable

1
2
3
4
5
6
7
NSManagedObjectContext.automaticallyMergesChangesFromParent
UIView.clipsToBounds
UIView.translatesAutoresizingMaskIntoConstraints
SwiftUI.Transaction.disablesAnimations
UIScrollView.scrollsToTop
NSView.isHidden
UndoManager.enableUndoRegistration

Updated at 2021-02-28 22:34:53

How to use with block configure in Swift

Issue #786

Sometimes we need to update some properties between objects, for example

1
2
3
book.name = updatedBook.name
book.page = updatedBook.page
book.publishedAt = updatedBook.publishedAt

Repeating the caller book is tedious and error-prone. In Kotlin, there is with block which is handy to access the receiver properties and methods without referring to it.

1
2
3
4
5
with(book) {
name = updatedBook.name
page = updatedBook.page
publishedAt = updatedBook.publishedAt
}

In Swift, there are no such thing, we can write some extension like

1
2
3
4
5
6
7
extension Book {
func update(with anotherBook: Book) {
name = anotherBook.name
page = anotherBook.page
publishedAt = anotherBook.publishedAt
}
}

Or simply, we can just use forEach with just that book

1
2
3
4
5
[book].forEach {
$0.name = updatedBook.name
$0.page = updatedBook.page
$0.publishedAt = updatedBook.publishedAt
}

How to use Core Data

Issue #785

Core Data

  • Responding to changes in a managed object context

    Calling mergeChanges on a managed object context will automatically refresh any managed objects that have changed. This ensures that your context always contains all the latest information. Note that you don’t have to call mergeChanges on a viewContext when you set its automaticallyMergesChangesFromParent property to true. In that case, Core Data will handle the merge on your behalf.

  • Using Core Data in the Background

    Managed objects retrieved from a context are bound to the same queue that the context is bound to.

  • The Laws of Core Data

    The ability to have a child MOC is neat in some corner cases. Let’s review the core functionality of MOCs in order to understand those cases:

  • MOCs load objects from its source (a PSC or another MOC)
  • MOCs save objects to its source (a PSC or another MOC)
  • MOCs enforce graph integrity when they save

Use Core Data as a local cache

When you save changes in a context, the changes are only committed “one store up.” If you save a child context, changes are pushed to its parent. Changes are not saved to the persistent store until the root context is saved. (A root managed object context is one whose parent context is nil.)

  • NSManagedObjectContext fetch

    An object that meets the criteria specified by request (it is an instance of the entity specified by the request, and it matches the request’s predicate if there is one) and that has been inserted into a context but which is not yet saved to a persistent store, is retrieved if the fetch request is executed on that context.

  • NSBatchDeleteRequest mergeChanges

    Changes are not reflected in the context”. So what you’re seeing is normal. Batch updates work directly on the persistent store file instead of going through the managed object context, so the context doesn’t know about them. When you delete the objects by fetching and then deleting, you’re working through the context, so it knows about the changes you’re making (in fact it’s performing those changes for you).

  • Does modifying a managed object in a parent context propagate down to child contexts when performing a child fetch in Core Data?

mainContext would fetch all the way to the persistent store. Fetches and objectWithID: only go as many levels as they need to.
It’s important to remember that when a context is created it’s a “snapshot” of the state of it’s parent. Subsequent changes to the parent will not be visible in the child unless the child is somehow invalidated

CloudKit

NSFetchedResultsController

How to fetch with background context

TLDR: Use NSFetchedResultsController in backgroundContext

  • If use Core Data as cache: convert to struct
  • If use NSManagedObject: use FetchRequest on viewContext

Approach 1: Treat Core Data as cache

Pros

  • backgroundContext.automaticallyMergesChangesFromParent
  • Use 1 shared background context to load and updates changes
  • Convert NSManagedObject to struct
  • Our struct can have additional logic and properties
  • Does not need to care about faulting

Cons

  • Need to update objectId When context.save, NSManagedObject ‘s objecteId is updated
  • Need to reload when underlying NSManagedObject changes
  • Need to handle relationship

Instead of plain NSFetchRequest, can use NSFetchedResultsController on background context. In its controllerDidChangeContent convert to struct

Approach 2: viewContext to persistent coordinator

1
viewContext -> persistent coordinator <- backgroundContext

Pros

  • viewContext.automaticallyMergesChangesFromParent
  • viewContext and background Context stems from persistent container
  • background context changes object
  • viewContext read only

Cons

  • viewContext blocks main thread

Approach 3: viewContext to background context

1
viewContext -> backgroundContext -> persistent coordinator

Pros

  • viewContext.automaticallyMergesChangesFromParent
  • viewContext reads from backgroundContext, no main queue block
  • backgroundContext changes

Cons

  • ???

Read more

Approach 4: background context -> mainContext -> backgroundContext -> persistent coordinator

https://stackoverflow.com/questions/35961936/child-nsmanagedobjectcontext-update-from-parent

1
2
3
RootContext (private queue) - saves to persistent store
MainContext (main queue) child of RootContext - use for UI (FRC)
WorkerContext (private queue) - child of MainContext - use for updates & inserts

Updated at 2021-02-28 07:58:23

How to listen to remote changes in CloudKit CoreData

Issue #783

Remove chane notification

Read Consuming Relevant Store Changes

If the import happens through a batch operation, the save to the store doesn’t generate an NSManagedObjectContextDidSave notification, and the view misses these relevant updates. Alternatively, the background context may save changes to the store that don’t affect the current view—for example, inserting, modifying, or deleting Shape objects. These changes do generate context save events, so your view context processes them even though it doesn’t need to.

Also, the doc mention NSPersistentStoreRemoteChangeNotificationOptionKey

1
2
3
4
5
6
7
8
9
10
11
let remoteChangeKey = "NSPersistentStoreRemoteChangeNotificationOptionKey"
description?.setOption(true as NSNumber,
forKey: remoteChangeKey)

NotificationCenter.default.addObserver(
self,
selector: #selector(fetchChanges),
name: NSNotification.Name(
rawValue: "NSPersistentStoreRemoteChangeNotification"),
object: persistentContainer.persistentStoreCoordinator
)

In the app, the value of NSPersistentStoreRemoteChangeNotificationPostOptionKey is NSPersistentStoreRemoteChangeNotificationOptionKey

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let container = NSPersistentCloudKitContainer(name: name)

guard let description = container.persistentStoreDescriptions.first else {
assertionFailure()
return nil
}

description.setOption(
true as NSNumber,
forKey: NSPersistentHistoryTrackingKey
)

description.setOption(
true as NSNumber,
forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey
)

Updated at 2021-02-25 22:42:37

How to listen to Published outside of SwiftUI view

Issue #782

Use $ to access Publisher

1
2
3
final class Store: ObservableObject {
@Published var showsSideWindow: Bool = false
}
1
2
3
4
5
6
7
8
9
10
var anyCancellables = Set<AnyCancellable>()

store.$showsSideWindow
.removeDuplicates()
.throttle(for: 0.2, scheduler: RunLoop.main, latest: true)
.receive(on: RunLoop.main)
.sink(receiveValue: { shows in
preferenceManager.reloadPosition(shows: shows)
})
.store(in: &anyCancellables)

Updated at 2021-02-25 21:56:42

How to filter non numeric digit from String in Swift

Issue #781

This sounds like an easy task, but a quick search on Stackoverflow results in this with highest votes https://stackoverflow.com/questions/29971505/filter-non-digits-from-string

CharacterSet.decimalDigits contains more than just digits

This splits a string by inverted set of decimalDigits and join them back.

1
2
3
4
5
6
extension String {
var digits: String {
return components(separatedBy: CharacterSet.decimalDigits.inverted)
.joined()
}
}

Reading decimalDigits

Informally, this set is the set of all characters used to represent the decimal values 0 through 9. These characters include, for example, the decimal digits of the Indic scripts and Arabic.

So decimalDigits does not only contain digits, but also some scripts in other languages. For normal cases this should not be a problem. As How many decimal digits are there, anyways? there are 610 valid characters in CharacterSet.decimalDigits. So be aware

1
2
3
4
5
6
7
8
9
10
11
12
13
let s = CharacterSet.decimalDigits

// U+0031 DIGIT ONE
s.contains("1") // true as expected

// U+1D7D9 MATHEMATICAL DOUBLE-STRUCK DIGIT ONE
s.contains("𝟙") // true!

// U+0967 DEVANAGARI DIGIT ONE
s.contains("१") // true!

// U+1811 MONGOLIAN DIGIT ONE
s.contains("᠑") // true!

Trimming

Another method is trimmingCharacters. Note that this removes only characters at the start and end of the string.

1
string.trimmingCharacters(in: CharacterSet(charactersIn: "0123456789").inverted)

Just filter

A boring but correct solution is to filter characters anywhere in the string.

1
string.filter("0123456789".contains)

This is more performant than components then join below

1
string.components(separatedBy: CharacterSet(charactersIn: "0123456789").inverted).joined()

Updated at 2021-02-25 08:52:11

How to build container view in SwiftUI

Issue #780

To make a container view that accepts child content, we use ViewBuilder

1
2
3
4
5
6
7
8
9
10
11
struct ContainerView<Content: View>: View {
let content: Content

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

var body: some View {
content
}
}

From Swift 5.4, it can synthesize the init, so we can declare resultBuilder for stored property

struct AwesomeContainerView<Content: View>: View {
    @ViewBuilder
    let content: Content

    var body: some View {
        content
    }
}

Updated at 2021-02-24 21:22:49

How to tune performance with ButtonBehavior in SwiftUI

Issue #779

With Xcode 12.4, macOS 11.0 app. Every time we switch the system dark and light mode, the CPU goes up to 100%. Instruments show that there’s an increasing number of ButtonBehavior

Screenshot 2021-02-24 at 10 12 05

Suspect State in a row in LazyVStack

Every cell has its own toggle state

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct Cell: View {
enum ToggleState {
case general
case request
case response
}

let item: Item
@State
private var toggleState: ToggleState = .general

func toggleButton(text: String, state: ToggleState) -> some View {
Button(action: { self.toggleState = state }) {
Text(text)
.foregroundColor(state == toggleState ? Color.label : Color.secondary)
.fontWeight(state == toggleState ? .bold : .regular)
}
.buttonStyle(BorderlessButtonStyle())
}
}

Removing the buttons fix the problem. The workaround is to use Text with onTapGesture

1
2
3
4
5
6
Text(text)
.foregroundColor(state == toggleState ? Color.label : Color.secondary)
.fontWeight(state == toggleState ? .bold : .regular)
.onTapGesture {
self.toggleState = state
}

Updated at 2021-02-24 10:10:41

How to use GroupBox in SwiftUI

Issue #778

For now using GroupBox has these issues in macOS

  • Prevents dragging scroll indicator to scroll
  • Switch from light to dark mode may cause 100% CPU usage

Updated at 2021-02-23 22:02:25

How to suppress selector warning in Swift

Issue #777

Sometimes we need to use dynamic selector and that triggers warning in Swift

1
Selector("updateWithCount:") // Use '#selector' instead of explicitly constructing a 'Selector'

In ObjC we can use clang macro to suppress, like below

1
2
3
4
5
6
7
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-implementations"

- (void) deprecated_objc_method_override {
}

#pragma clang diagnostic pop

But in Swift, we can just use a dummy NSObject that has the needed methods, like

1
2
3
final class Dummy: NSObject {
@objc func update(count: Int) {}
}

#selector is just a safer way to construct Selector, they all yield same result as String

1
2
Selector("updateWithCount:") // updateWithCount:
#selector(Dummy.update(count:)) // updateWithCount:

Updated at 2021-02-18 10:11:13

How to make simple search bar in SwiftUI

Issue #776

We need to use a custom Binding to trigger onChange as onEditingChanged is only called when the user selects the textField, and onCommit is only called when return or done button on keyboard is tapped.

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
import UIKit
import SwiftUI
import EasySwiftUI

struct SearchBar: View {
@Binding
var searchText: String
let onChange: () -> Void
@State
private var showsCancelButton: Bool = false

var body: some View {
return HStack {
textField
cancelButton
}
}

private var searchTextBinding: Binding<String> {
Binding<String>(get: {
searchText
}, set: { newValue in
DispatchQueue.main.async {
searchText = newValue
onChange()
}
})
}

private var textField: some View {
HStack {
Image(systemName: SFSymbol.magnifyingglass.rawValue)

TextField("Search", text: searchTextBinding, onEditingChanged: { isEditing in
withAnimation {
self.showsCancelButton = true
}
onChange()
}, onCommit: {
// No op
})
.foregroundColor(.primary)

Button(action: {
self.searchText = ""
}) {
Image(systemName: SFSymbol.xmarkCircleFill.rawValue)
.opacity(searchText == "" ? 0 : 1)
}
}
.padding(EdgeInsets(top: 8, leading: 6, bottom: 8, trailing: 6))
.foregroundColor(.secondary)
.background(Color(.systemBackground))
.cornerRadius(10.0)
}

@ViewBuilder
private var cancelButton: some View {
if showsCancelButton {
Button("Cancel") {
UIApplication.shared.endEditing(true)
withAnimation {
self.searchText = ""
self.showsCancelButton = false
}
onChange()
}
.foregroundColor(Color(.systemBlue))
}
}
}

extension UIApplication {
func endEditing(_ force: Bool) {
self.windows
.filter{$0.isKeyWindow}
.first?
.endEditing(force)
}
}

How to add home screen quick action in SwiftUI

Issue #774

Start by defining your quick actions. You can use UIApplicationShortcutIcon(type:) for predefined icons, or use UIApplicationShortcutIcon(systemImageName:) for SFSymbol

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
enum QuickAction: String {
case readPasteboard
case clear

var shortcutItem: UIApplicationShortcutItem {
switch self {
case .readPasteboard:
return UIApplicationShortcutItem(
type: rawValue,
localizedTitle: "Read Pasteboard",
localizedSubtitle: "",
icon: UIApplicationShortcutIcon(type: .add),
userInfo: nil
)
case .clear:
return UIApplicationShortcutItem(
type: rawValue,
localizedTitle: "Clear Pasteboard",
localizedSubtitle: "",
icon: UIApplicationShortcutIcon(systemImageName: SFSymbol.wind.rawValue),
userInfo: nil
)
}
}
}

Add a service to store selected quick action. I usually make this conform to ObservableObject to be able to bind to SwiftUI views later

1
2
final class QuickActionService: ObservableObject {
var shortcutItem: UIApplicationShortcutItem?

Expose AppDelegate and SceneDelegate to your SwiftUI App. Listen to scenePhase to add dynamic items

From Define Dynamic Quick Actions

Set dynamic screen quick actions at any point, but the sample sets them in the sceneWillResignActive(_:) function of the scene delegate. During the transition to a background state is a good time to update any dynamic quick actions, because the system executes this code before the user returns to the Home Screen.

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
@main
struct PastePaliOSApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self)
var appDelegate
@Environment(\.scenePhase)
var scenePhase

var body: some Scene {
WindowGroup {
main
}
.onChange(of: scenePhase) { scenePhase in
switch scenePhase {
case .background:
addDynamicQuickActions()
case .active:
QuickActionService.shared.perform()
default:
break
}
}
}

private func addDynamicQuickActions() {
UIApplication.shared.shortcutItems = [
QuickAction.readPasteboard.shortcutItem,
QuickAction.clear.shortcutItem
]
}
}

Quick actions are notified in 2 cases

  • If the app isn’t already loaded, it’s launched and passes details of the shortcut item in through the connectionOptions parameter of the scene(_:willConnectTo:options:) function in AppDelegate
  • If your app is already loaded, the system calls the windowScene(_:performActionFor:completionHandler:) function of your SceneDelegate

Therefore we need to handle both cases.

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
final class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
configurationForConnecting connectingSceneSession: UISceneSession,
options: UIScene.ConnectionOptions
) -> UISceneConfiguration {
if let shortcutItem = options.shortcutItem {
QuickActionService.shared.shortcutItem = shortcutItem
}

let sceneConfiguration = UISceneConfiguration(
name: "Default",
sessionRole: connectingSceneSession.role
)
sceneConfiguration.delegateClass = SceneDelegate.self

return sceneConfiguration
}
}

private final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
func windowScene(
_ windowScene: UIWindowScene,
performActionFor shortcutItem: UIApplicationShortcutItem,
completionHandler: @escaping (Bool) -> Void
) {
QuickActionService.shared.shortcutItem = shortcutItem
completionHandler(true)
}

Read more

For more please consult official Apple docs and design


Updated at 2021-02-10 06:43:19

How to use EquatableView in SwiftUI

Issue #773

From John Harper ‘s tweet

SwiftUI assumes any Equatable.== is a true equality check, so for POD views it compares each field directly instead (via reflection). For non-POD views it prefers the view’s == but falls back to its own field compare if no ==. EqView is a way to force the use of ==.

When it does the per-field comparison the same rules are applied recursively to each field (to choose direct comparison or == if defined). (POD = plain data, see Swift’s _isPOD() function.)

Read more

How to add new property in Codable struct in SwiftUI

Issue #772

I use Codable structs in my apps for preferences, and bind them to SwiftUI views. If we add new properties to existing Codable, it can’t decode with old saved json as we require new properties. We can either do cutom decoding with container, but this can result in lots more code and mistakes if we have many properties inside our struct.

The quick workaround is to declare new properties as optional, and use a computed property to wrap that. The good news is Binding works with computed properties too, from the outside all looks like struct properties to SwiftUI

1
2
3
4
5
6
struct Preference: Codable {
var _redacts: Bool? = false
var redacts: Bool {
get { _redacts ?? false }
set { _redacts = newValue }
}

How to handle escape in NSTextField in SwiftUI

Issue #770

Handle cancelOperation somewhere up in responder chain

1
2
3
4
5
6
7
8
class MyWindow: NSWindow {
let keyHandler = KeyHandler()

override func cancelOperation(_ sender: Any?) {
super.cancelOperation(sender)
keyHandler.onEvent(.esc)
}
}

How to fit ScrollView to content in SwiftUI

Issue #769

If we place ScrollView inside HStack or VStack, it takes all remaining space. To fit ScrollView to its content, we need to get its content size and constrain ScrollView size.

Use a GeometryReader as Scrollview content background, and get the local frame

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
import SwiftUI

struct HSearchBar: View {
@State
private var scrollViewContentSize: CGSize = .zero

var body: some View {
HStack {
searchButton
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
ForEach(store.collections) { collection in
collectionCell(collection)
}
}
.background(
GeometryReader { geo -> Color in
DispatchQueue.main.async {
scrollViewContentSize = geo.size
}
return Color.clear
}
)
}
.frame(
maxWidth: scrollViewContentSize.width
)
}
}
}

How to show modal window in SwiftUI for macOS

Issue #768

Use custom NSWindow, set level in becomeKey and call NSApp.runModal to show modal

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
final class ModalWindow: NSWindow {
override func becomeKey() {
super.becomeKey()

level = .statusBar
}

override func close() {
super.close()

NSApp.stopModal()
}
}

let window = ModalWindow(
contentRect: .zero,
styleMask: [.titled, .closable],
backing: .buffered,
defer: false
)

window.titlebarAppearsTransparent = true
window.title = "Manage collections"

window.center()
window.isReleasedWhenClosed = false
self.window = window
let view = CollectionSettingsView(store: Store.shared)
.padding()
.frame(
width: Constants.settingsViewWidth,
height: 350,
alignment: .topLeading
)
let hosting = NSHostingView(rootView: view)
window.contentView = hosting
hosting.autoresizingMask = [.width, .height]

NSApp.runModal(for: window)

How to use ViewBuilder in SwiftUI

Issue #767

SwiftUI ‘s ViewBuilder is a custom parameter attribute that constructs views from closures.

It is available in body and most SwiftUI modifiers

1
2
3
4
5
6
public protocol View {
associatedtype Body : View
@ViewBuilder var body: Self.Body { get }
}

public func contextMenu<MenuItems>(@ViewBuilder menuItems: () -> MenuItems) -> some View where MenuItems : View

In these ViewBuilder enabled places we can perform conditional logic to construct views. For example here in our SampleView, we have switch statement in body

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct SampleView: View {
enum Position {
case top, bottom, left, right
}

let position: Position

var body: some View {
switch position {
case .top:
Image(systemName: SFSymbol.person.rawValue)
default:
EmptyView()
}
}

var profile: some View {
if true {
return Image(systemName: SFSymbol.person.rawValue)
}
}
}

ViewBuilder applies to both property and function. If we want to have the same logic style as in body in our custom property or methods, we can annotate with ViewBuilder. This works like magic, SwiftUI can determine the types of our expression.

1
2
3
4
5
6
7
8
9
10
11
extension SampleView {
@ViewBuilder
func profile2(position: Position) -> some View {
switch position {
case .top:
Image(systemName: SFSymbol.person.rawValue)
default:
EmptyView()
}
}
}

Use ViewBuilder to construct View

We can use ViewBuiler as our parameter that constructs View. For example we can build an IfLet that construct View with optional check.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public struct IfLet<T, Content: View>: View {
let value: T?
let content: (T) -> Content

public init(_ value: T?, @ViewBuilder content: @escaping (T) -> Content) {
self.value = value
self.content = content
}

public var body: some View {
if let value = value {
content(value)
}
}
}

With ViewBuilder we can apply logic inside our closure

1
2
3
4
5
6
7
8
9
10
11
12
13
struct EmailView: View {
let email: String?

var body: some View {
IfLet(email) { email in
if email.isEmpty {
Circle()
} else {
Text(email)
}
}
}
}

Use ViewBuilder where we can’t use closure

In some modifers like overlay, SwiftUI expects a View, not a closure that returns a View. There we cannot use additional logic

1
2
extension View {
@inlinable public func overlay<Overlay>(_ overlay: Overlay, alignment: Alignment = .center) -> some View where Overlay : View

The below won’t work as we can’t do conditional statement in overlay modifier

1
2
3
4
5
6
7
8
9
10
11
12
struct MessageView: View {
let showsHUD: Bool

var body: some View {
Text("Message")
.overlay(
if showsHUD {
Text("HUD")
}
)
}
}

But we can make something like MakeView that provides a ViewBuilder closure

1
2
3
4
5
6
7
8
9
10
11
public struct MakeView<Content: View>: View {
let content: Content

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

public var body: some View {
content
}
}

So we can use a conditional statement in any modifier that does not accept ViewModifier

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct MessageView: View {
let showsHUD: Bool

var body: some View {
Text("Message")
.overlay(
MakeView {
if showsHUD {
Text("HUD")
}
}
)
}
}

How to use custom Key for NSCache

Issue #766

Need to use a class, best is to subclass from NSObject

1
let cache = NSCache<Key, UIImage>()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
final class Key: NSObject {
override func isEqual(_ object: Any?) -> Bool {
guard let other = object as? Key else {
return false
}
return url == other.url
&& size == other.size
}

override var hash: Int {
return url.hashValue ^ Int(size.width) ^ Int(size.height)
}

let url: URL
let size: CGSize

init(url: URL, size: CGSize) {
self.url = url
self.size = size
}
}

How to show multiple popover in SwiftUI

Issue #765

In SwiftUI currently, it’s not possible to attach multiple .popover to the same View. But we can use condition to show different content

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
struct ClipboardCell: View {
enum PopoverStyle {
case raw
case preview
}

@Binding
var showsPopover: Bool
@Binding
var popoverStyle: PopoverStyle

var body: some View {
VStack {
header
content
.popover(isPresented: $showsPopover) {
switch popoverStyle {
case .raw:
ViewRawView(item: item)
case .preview:
PreviewView(item: item)
}
}
footer
}
}
}

How to handle keyDown in SwiftUI for macOS

Issue #764

Use a custom KeyAwareView that uses an NSView that checks for keyDown method. In case we can’t handle certain keys, call super.keyDown(with: event)

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
import SwiftUI
import KeyboardShortcuts

struct KeyAwareView: NSViewRepresentable {
let onEvent: (Event) -> Void

func makeNSView(context: Context) -> NSView {
let view = KeyView()
view.onEvent = onEvent
DispatchQueue.main.async {
view.window?.makeFirstResponder(view)
}
return view
}

func updateNSView(_ nsView: NSView, context: Context) {}
}

extension KeyAwareView {
enum Event {
case upArrow
case downArrow
case leftArrow
case rightArrow
case space
case delete
case cmdC
}
}

private class KeyView: NSView {
var onEvent: (KeyAwareView.Event) -> Void = { _ in }

override var acceptsFirstResponder: Bool { true }
override func keyDown(with event: NSEvent) {
switch Int(event.keyCode) {
case KeyboardShortcuts.Key.delete.rawValue:
onEvent(.delete)
case KeyboardShortcuts.Key.upArrow.rawValue:
onEvent(.upArrow)
case KeyboardShortcuts.Key.downArrow.rawValue:
onEvent(.downArrow)
case KeyboardShortcuts.Key.leftArrow.rawValue:
onEvent(.leftArrow)
case KeyboardShortcuts.Key.rightArrow.rawValue:
onEvent(.rightArrow)
case KeyboardShortcuts.Key.space.rawValue:
onEvent(.space)
case KeyboardShortcuts.Key.c.rawValue where event.modifierFlags.contains(.command):
onEvent(.cmdC)
default:
super.keyDown(with: event)
}
}
}

Then we can place this as a background

1
2
3
4
LazyVStack {

}
.background(KeyAwareView(onEvent: {}))

Updated at 2021-01-29 20:55:39

How to extend custom View in SwiftUI

Issue #763

I usually break down a big struct into smaller views and extensions. For example I have a ClipboardCell that has a lot of onReceive so I want to move these to another component.

One way to do that is to extend ClipboardCell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct ClipboardCell: View {
let isSelected: Bool
@State var showsPreview: Bool
@State var showsViewRaw: Bool
let onCopy: () -> Void
let onDelete: () -> Void
}

extension ClipboardCell {
func onReceiveKeyboard() -> some View {
self.onReceive(
NotificationCenter.default
.publisher(for: .didKeyboardCopyItem)
.receive(on: RunLoop.main),
perform: { note in
onCopy()
}
)
}
}

but then when we want to use this, we get some View has no member onReceiveKeyboard as self after some Swift modifier becomes some View, unless we call onReceiveKeyboard first

1
2
3
4
5
6
7
struct ClipboardCell: View {
var body: some View {
self
.padding()
.onReceiveKeyboard()
}
}

Use ViewModifier

The SwiftUI is to use ViewModifier where we can inject Binding and functions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct ClipboardCellOnKeyboardModifier: ViewModifier {
let isSelected: Bool
@Binding var showsPreview: Bool
@Binding var showsViewRaw: Bool
let onCopy: () -> Void
let onDelete: () -> Void

func body(content: Content) -> some View {
content.onReceive(
NotificationCenter.default
.publisher(for: .didKeyboardCopyItem)
.receive(on: RunLoop.main),
perform: { _ in
guard isSelected else { return }
onCopy()
}
)
}
}
`

Then we can consume it and pass parameters

1
2
3
4
5
6
7
8
9
10
11
12
struct ClipboardCell: View {
var body: some View {
self
.padding()
.modifier(
ClipboardCellOnKeyboardModifier(
showsPreview: Binding<Bool>(get: {}, set: {}) ,
showsViewRaw: Binding<Bool>(get: {}, set: {})
)
)
}
}

Pass State and Binding

For now SwiftUI seems to have a bug that ViewModifier does not listen to onReceive, we can extend generic View and pass parameters instead

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
extension View {
func onClipboardCellReceiveKeyboard(
isSelected: Bool,
showsPreview: Binding<Bool>,
showsViewRaw: Binding<Bool>,
onCopy: () -> Void,
onDelete: () -> Void
) -> some View {
self.onReceive(
NotificationCenter.default
.publisher(for: .didKeyboardCopyItem)
.receive(on: RunLoop.main),
perform: { _ in
guard isSelected else { return }
onCopy()
}
)

Use ObservableObject

Another way is to use an ObservableObject and encapsulate logic and state in there, and share this across views that want to consume this set of data, just like a ViewModel

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
import SwiftUI

final class ItemsHolder: ObservableObject {
@Published var items: [ClipboardItem] = []
@Published var selectedItems = Set<ClipboardItem>()
@Published var agos: [UUID: String] = [:]

func updateAgos() {
agos.removeAll()
for item in items {
agos[item.id] = Formattes.ago(date: item.createdAt)
}
}

func update(items: [ClipboardItem]) {
self.items = items
.sorted(by: { $0.createdAt > $1.createdAt })
updateAgos()
}
}

struct ClipboardCell: View {
@StateObject var itemsHolder = ItemsHolder()

var body: some View {
list.onReceive(
NotificationCenter.default
.publisher(for: .didKeyboardCopyItem)
.receive(on: RunLoop.main),
perform: { note in
itemsHolder.onCopy()
}
)
}
}

Updated at 2021-01-29 12:51:52

How to use Sparkle for macOS app

Issue #762

Install Sparkle

  • For now, the latest stable version is 1.24.0 which supports CocoaPods OK, but still, have issues with SPM. Support non sandboxed apps
  • Version 2.0.0 is in beta and supports sandboxed apps

To install, use CocoaPods

1
2
3
4
5
6
7
8
9
platform :osx, '11.0'

target 'MyApp' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!

pod 'Sparkle'

end

Sign

In your target, choose Signing & Capability tab, change Signing Certificate from Locally to Development for code sign to work for embedded frameworks

Specify SUUpdater

Follow Sparkle documentation, let’s add an Object to Application Scene in Main.storyboard and specify SUUpdater class

In AppDelegate

1
2
3
4
@IBAction func checkForUpdates(_ sender: Any) {
let updater = SUUpdater.shared()
updater?.checkForUpdates(self)
}

Additionally, we can add an NSMenuItem and drag it to our SUUpdater object checkForUpdates method

In Info.plist, add SUFeedURL with a link to your cast file

1
2
<key>SUFeedURL</key>
<string>https://onmyway133.com/MyAppCast.xml</string>

Configure cast file

I usually upload my cast to S3 or GitHub. For GitHub with raw URL, remember that there is 5 minutes cache by default

If you follow sample cast file https://sparkle-project.org/files/sparkletestcast.xml

Notice that sparkle:version="2.0" is CFBundleVersion which is your build number. You need to also specify sparkle:shortVersionString which is CFBundleShortVersionString your version number

1
sparkle:shortVersionString="2.0.1"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
<title>Sparkle Test App Changelog</title>
<link>http://sparkle-project.org/files/sparkletestcast.xml</link>
<description>Most recent changes with links to updates.</description>
<language>en</language>
<item>
<title>Version 2.0</title>
<description>
<![CDATA[
<ul>
<li>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</li>
<li>Suspendisse sed felis ac ante ultrices rhoncus. Etiam quis elit vel nibh placerat facilisis in id leo.</li>
<li>Vestibulum nec tortor odio, nec malesuada libero. Cras vel convallis nunc.</li>
<li>Suspendisse tristique massa eget velit consequat tincidunt. Praesent sodales hendrerit pretium.</li>
</ul>
]]>
</description>
<pubDate>Sat, 26 Jul 2014 15:20:11 +0000</pubDate>
<enclosure url="https://sparkle-project.org/files/Sparkle%20Test%20App.zip" sparkle:version="2.0" length="107758" type="application/octet-stream" sparkle:dsaSignature="MCwCFCdoW13VBGJWIfIklKxQVyetgxE7AhQTVuY9uQT0KOV1UEk21epBsGZMPg==" />
</item>
</channel>
</rss>

Updated at 2021-01-26 06:34:38

How to use ScrollViewReader in SwiftUI

Issue #761

Explicitly specify id

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ScrollView {
ScrollViewReader { proxy in
LazyVStack(spacing: 10) {
ForEach(items) { item in
Cell(item: item)
.id(item.id)
}
}
.padding()
.onReceiveKeyboard(onNext: {
onNext()
if let item = selectedItem {
proxy.scrollTo(item.id, anchor: .center)
}
}, onPrevious: {
onPrevious()
if let item = selectedItem {
proxy.scrollTo(item.id, anchor: .center)
}
})
}
}

I usually extract ScrollViewReader into a helper function that use onChange to react to state changes

1
2
3
4
5
6
7
8
9
10
11
 func scrollViewReader<Content: View>(@ViewBuilder content: @escaping () -> Content) -> some View {
ScrollViewReader { proxy in
content()
.onChange(of: itemsHolder.focusItem) { item in
guard let item = item else { return }
withAnimation {
proxy.scrollTo(item.id, anchor: .center)
}
}
}
}

Updated at 2021-02-21 19:14:01

How to handle keyDown in NSResponder

Issue #760

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 AppKit
import Omnia

class MyWindow: NSWindow {
override func keyDown(with event: NSEvent) {
super.keyDown(with: event)

if isKey(NSDeleteCharacter, event: event) {
NotificationCenter.default.post(Notification(name: .didKeyboardDeleteItem))
} else if isKey(NSUpArrowFunctionKey, event: event) {
print("up")
} else if isKey(NSDownArrowFunctionKey, event: event) {
print("down")
} else if isKey(NSLeftArrowFunctionKey, event: event) {
print("left")
} else if isKey(NSRightArrowFunctionKey, event: event) {
print("right")
}
}

private func isKey(_ key: Int, event: NSEvent) -> Bool {
if let scalar = UnicodeScalar(key) {
return event.characters == String(scalar)
} else {
return false
}
}
}

Another way is to listen to

1
2
3
4
5
6
NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in
if handled(with: $0) {
return nil
}
return event
}

Or create our own inspector

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
class MyWindow: NSWindow {
override func keyDown(with event: NSEvent) {
if !EventService.shared.inspect(event) {
super.keyDown(with: event)
}
}

override var canBecomeKey: Bool { true }
override var canBecomeMain: Bool { true }
override var acceptsFirstResponder: Bool { true }
override func becomeFirstResponder() -> Bool { true }
}

import AppKit
import Omnia
import KeyboardShortcuts

final class EventService {
static let shared = EventService()

private var combos: [Combo] = []
private let debouncer = Debouncer(delay: 0.1)

func register(_ combo: Combo) {
combos.append(combo)
}

func inspect(_ event: NSEvent) -> Bool {
for combo in combos {
if combo.match(event) {
self.debouncer.run {
combo.action()
}
return true
}
}

return false
}
}

extension EventService {
struct Combo {
let modifier: NSEvent.ModifierFlags?
let keyCode: KeyboardShortcuts.Key
let action: () -> Void

func match(_ event: NSEvent) -> Bool {
if let modifier = modifier,
!event.modifierFlags.contains(modifier) {
return false
}

if event.keyCode != keyCode.rawValue {
return false
}

return true
}
}
}

Updated at 2021-01-25 20:05:08

How to handle NSSearchToolbarItem in macOS 11

Issue #758

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
extension NSToolbarItem.Identifier {
static let searchItem: NSToolbarItem.Identifier = NSToolbarItem.Identifier("SearchItem")
}

let searchItem = NSSearchToolbarItem(itemIdentifier: .searchItem)

extension AppDelegate: NSToolbarDelegate {
func toolbar(
_ toolbar: NSToolbar,
itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier,
willBeInsertedIntoToolbar flag: Bool
) -> NSToolbarItem? {
switch itemIdentifier {
case .searchItem:
searchItem.searchField.delegate = self
return searchItem
}
}


extension AppDelegate: NSSearchFieldDelegate {
func control(
_ control: NSControl,
textView: NSTextView,
doCommandBy commandSelector: Selector
) -> Bool {
if (commandSelector == #selector(NSResponder.insertNewline(_:))) {
print("enter")
return true
}

return false
}
}

How to do launch at login for macOS apps

Issue #757

  • Use SMLoginItemSetEnabled from Service Management framework
  • Use a helper background app that checks and invokes our main application
  • Copy our helper app into Library/LoginItems
1
helper_dir="$BUILT_PRODUCTS_DIR/$CONTENTS_FOLDER_PATH/Library/LoginItems"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
final class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
let bundleId = Bundle.main.bundleIdentifier!
// TODO: Make this more strict by only replacing at the end
let mainBundleId = bundleId.replacingOccurrences(of: "-LaunchAtLoginHelper", with: "")

// Ensure the app is not already running
guard NSRunningApplication.runningApplications(withBundleIdentifier: mainBundleId).isEmpty else {
NSApp.terminate(nil)
return
}

let pathComponents = (Bundle.main.bundlePath as NSString).pathComponents
let mainPath = NSString.path(withComponents: Array(pathComponents[0...(pathComponents.count - 5)]))
NSWorkspace.shared.launchApplication(mainPath)
NSApp.terminate(nil)
}
}

Read more

How to fix overlapped navigation titles in SwiftUI

Issue #756

1
2
3
4
5
6
7
8
9
extension NavigationLink {
func fixOverlap() -> AnyView {
if UIDevice.current.userInterfaceIdiom == .phone {
return self.isDetailLink(false).erase()
} else {
return self.erase()
}
}
}

Read more


Updated at 2021-01-20 21:59:41