How to declare commands in Xcode extensions

Issue #638

Use commandDefinitions in XCSourceEditorExtension.

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 Foundation
import XcodeKit

class SourceEditorExtension: NSObject, XCSourceEditorExtension {
func extensionDidFinishLaunching() {

}

var commandDefinitions: [[XCSourceEditorCommandDefinitionKey: Any]] {
func makeDef(
_ className: String,
_ commandName: String
) -> [XCSourceEditorCommandDefinitionKey: Any] {
guard let bundleId = Bundle(for: type(of: self)).bundleIdentifier else { return [:] }

return [
XCSourceEditorCommandDefinitionKey.identifierKey: bundleId + className,
XCSourceEditorCommandDefinitionKey.classNameKey: className,
XCSourceEditorCommandDefinitionKey.nameKey: commandName
]
}

return [
makeDef(TypeCommand.className(), "Type"),
makeDef(ReloadCommand.className(), "Reload"),
]
}
}

There is a weird crash that we can’t seem to declare functions or use commandDefinitions, the workaround is to declare in plist

Read more

How to disable ring type in TextField in SwiftUI

Issue #636

Normally we can just wrap NSTextField

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
struct SearchTextField: NSViewRepresentable {
@Binding var text: String
var hint: String
var onCommit: (String) -> Void

func makeNSView(context: NSViewRepresentableContext<SearchTextField>) -> NSTextField {
let tf = NSTextField()
tf.focusRingType = .none
tf.isBordered = false
tf.isEditable = true
tf.isSelectable = true
tf.drawsBackground = false
tf.delegate = context.coordinator
tf.font = NSFont(name: OpenSans.bold.rawValue, size: 14)
tf.placeholderString = hint
return tf
}

func updateNSView(
_ nsView: NSTextField,
context: NSViewRepresentableContext<SearchTextField>
) {
nsView.font = NSFont(name: OpenSans.bold.rawValue, size: 14)
nsView.stringValue = text
}

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

class Coordinator: NSObject, NSTextFieldDelegate {
let parent: SearchTextField
init(parent: SearchTextField) {
self.parent = parent
}

func controlTextDidChange(_ obj: Notification) {
let textField = obj.object as! NSTextField
parent.text = textField.stringValue
}

func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
if (commandSelector == #selector(NSResponder.insertNewline(_:))) {
self.parent.onCommit(textView.string)
return true
} else {
return false
}
}
}
}

But there is a weird Appstore rejection where the textfield is not focusable. The workaround is to use TextField

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
extension NSTextField {
open override var focusRingType: NSFocusRingType {
get { .none }
set { }
}
}

TextField(
"What's next?",
text: $text,
onCommit: { self.onAdd(self.text) }
)
.font(.system(size: 14, weight: .semibold, design: .rounded))
.textFieldStyle(PlainTextFieldStyle())
.padding(1)
.background(RoundedRectangle(cornerRadius: 2).stroke(Color.white))

How to handle enter key in NSTextField

Issue #635

1
textField.delegate = self
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
NSTextFieldDelegate

func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
if (commandSelector == #selector(NSResponder.insertNewline(_:))) {
// Do something against ENTER key
print("enter")
return true
} else if (commandSelector == #selector(NSResponder.deleteForward(_:))) {
// Do something against DELETE key
return true
} else if (commandSelector == #selector(NSResponder.deleteBackward(_:))) {
// Do something against BACKSPACE key
return true
} else if (commandSelector == #selector(NSResponder.insertTab(_:))) {
// Do something against TAB key
return true
} else if (commandSelector == #selector(NSResponder.cancelOperation(_:))) {
// Do something against ESCAPE key
return true
}

// return true if the action was handled; otherwise false
return false
}

How to decode with default case for enum in Swift

Issue #634

1
2
3
4
5
6
7
8
9
10
public enum Weapon: String, Decodable {
case sword = "SWORD"
case gun = "GUN"
case unknown = "UNKNOWN"

public init(from decoder: Decoder) throws {
let rawValue = try decoder.singleValueContainer().decode(String.self)
self = Weapon(rawValue: rawValue) ?? .unknown
}
}

How to conditionally apply modifier in SwiftUI

Issue #633

Use autoclosure and AnyView

1
2
3
4
5
6
7
8
9
10
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public extension View {
func applyIf<T: View>(_ condition: @autoclosure () -> Bool, apply: (Self) -> T) -> AnyView {
if condition() {
return apply(self).erase()
} else {
return self.erase()
}
}
}
1
2
3
4
5
6
7
8
9
10
Button(action: onSearch) {
Image("search")
.resizable()
.styleButton()
.overlay(ToolTip("Search"))
}
.buttonStyle(BorderlessButtonStyle())
.applyIf(showsSearch, apply: {
$0.foregroundColor(Color.orange)
})

How to toggle with animation in SwiftUI

Issue #632

Use Group

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private func makeHeader() -> some View {
Group {
if showsSearch {
SearchView(
onSearch: onSearch
)
.transition(.move(edge: .leading))
} else {
InputView(
onAdd: onAdd
)
.transition(.move(edge: .leading))
}
}
}

withAnimation {
self.showsSearch.toggle()
}

How to use background in iOS

Issue #631

beginBackgroundTask

https://developer.apple.com/documentation/uikit/app_and_environment/scenes/preparing_your_ui_to_run_in_the_background/extending_your_app_s_background_execution_time

When your app moves to the background, the system calls your app delegate’s applicationDidEnterBackground(_:) method. That method has five seconds to perform any tasks and return. Shortly after that method returns, the system puts your app into the suspended state. For most apps, five seconds is enough to perform any crucial tasks, but if you need more time, you can ask UIKit to extend your app’s runtime.

You extend your app’s runtime by calling the beginBackgroundTask(withName:expirationHandler:) method. Calling this method gives you extra time to perform important tasks.

BackgroundTasks

Use the BackgroundTasks framework to keep your app content up to date and run tasks requiring minutes to complete while your app is in the background. Longer tasks can optionally require a powered device and network connectivity.

Register launch handlers for tasks when the app launches and schedule them as required. The system will launch your app in the background and execute the tasks.

The main API for using this framework is the BGTaskScheduler . This API constantly monitors the system state such as battery level, background usage, and more, so it chooses the optimal time to run your tasks.

To use this API, you begin working when your app is on the foreground. You need to create Background task request. The framework provides an abstract class BGTask, you never use this task directly. Instead, the framework provides two concrete subclasses you can interact with: BGProcessingTask, for long running and maintenance tasks such backup and cleanup, and BGAppRefreshTask to keep your app up-to-date throughout the day.

URLSession background

When you create your background download or upload tasks with URLSession, you’re actually scheduling a download (or upload) with the ‘nsurlsessiond’ which is a daemon service that runs as a separate process.

How to show context popover from SwiftUI for macOS

Issue #630

For SwiftUI app using NSPopover, to show context popover menu, we can ask for windows array, get the _NSPopoverWindow and calculate the position. Note that origin of macOS screen is bottom left

1
2
3
4
(lldb) po NSApp.windows
▿ 2 elements
- 0 : <NSStatusBarWindow: 0x101a02700>
- 1 : <_NSPopoverWindow: 0x101c01060>
1
2
3
4
5
6
7
8
9
10
let handler = MenuHandler()
handler.add(title: "About", action: onAbout)
handler.add(title: "Quit", action: onQuit)

guard let window = NSApp.windows.last else { return }
let position = CGPoint(
x: window.frame.maxX - 100,
y: window.frame.minY + 80
)
handler.menu.popUp(positioning: nil, at: position, in: nil)

How to make segmented control in SwiftUI for macOS

Issue #629

Use Picker with SegmentedPickerStyle.

1
2
3
4
5
6
7
8
9
10
11
12
Picker(selection: $preferenceManager.preference.display, label: EmptyView()) {
Image("grid")
.resizable()
.padding()
.tag(0)
Image("list")
.resizable()
.tag(1)
}.pickerStyle(SegmentedPickerStyle())
.frame(width: 50)
.padding(.leading, 16)
.padding(.trailing, 24)

Alternatively, we can make custom NSSegmentedControl

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

struct MySegmentControl: NSViewRepresentable {
func makeCoordinator() -> MySegmentControl.Coordinator {
Coordinator(parent: self)
}

func makeNSView(context: NSViewRepresentableContext<MySegmentControl>) -> NSSegmentedControl {
let control = NSSegmentedControl(
images: [
NSImage(named: NSImage.Name("grid"))!,
NSImage(named: NSImage.Name("list"))!
],
trackingMode: .selectOne,
target: context.coordinator,
action: #selector(Coordinator.onChange(_:))
)
return control
}

func updateNSView(_ nsView: NSSegmentedControl, context: NSViewRepresentableContext<MySegmentControl>) {

}

class Coordinator {
let parent: MySegmentControl
init(parent: MySegmentControl) {
self.parent = parent
}

@objc
func onChange(_ control: NSSegmentedControl) {

}
}
}

How to iterate over XCUIElementQuery in UITests

Issue #628

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
extension XCUIElementQuery: Sequence {
public typealias Iterator = AnyIterator<XCUIElement>
public func makeIterator() -> Iterator {
var index = UInt(0)
return AnyIterator {
guard index < self.count else { return nil }

let element = self.element(boundBy: Int(index))
index = index + 1
return element
}
}
}

extension NSPredicate {
static func label(contains string: String) -> NSPredicate {
NSPredicate(format: "label CONTAINS %@", string)
}
}

let books = app.collectionViews.cells.matching(
NSPredicate.label(contains: "book")
)

for book in books {

}

How to check if NSColor is light

Issue #627

Algorithm from https://www.w3.org/WAI/ER/WD-AERT/#color-contrast

1
2
3
4
5
6
7
8
9
10
11
extension NSColor {
var isLight: Bool {
guard
let components = cgColor.components,
components.count >= 3
else { return false }

let brightness = ((components[0] * 299) + (components[1] * 587) + (components[2] * 114)) / 1000
return brightness > 0.5
}
}

Then we can apply contrast color for our Text

1
2
3
4
5
6
7
8
9
10
extension Text {
func applyColorBaseOnBackground(_ color: NSColor?) -> some View {
guard let color = color else { return self }
if color.isMyLight {
return self.foregroundColor(Color.black)
} else {
return self
}
}
}

How to trigger onAppear in SwiftUI for macOS

Issue #626

SwiftUI does not trigger onAppear and onDisappear like we expect. We can use NSView to trigger

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

struct AppearAware: NSViewRepresentable {
var onAppear: () -> Void

func makeNSView(context: NSViewRepresentableContext<AppearAware>) -> AwareView {
let view = AwareView()
view.onAppear = onAppear
return view
}

func updateNSView(_ nsView: AwareView, context: NSViewRepresentableContext<AppearAware>) {

}
}

final class AwareView: NSView {
private var trigged: Bool = false
var onAppear: () -> Void = {}

override func viewDidMoveToSuperview() {
super.viewDidMoveToSuperview()

guard !trigged else { return }
trigged = true
onAppear()
}
}

Then we can use it as an hidden view, like in a ZStack

1
2
3
4
5
6
7
8
ZStack {
AppearAware(onAppear: {
LocalImageCache.shared.load(url: url) { image in
self.image = image
}
})
Image(image)
}

How to force refresh in ForEach in SwiftUI for macOS

Issue #625

For some strange reasons, content inside ForEach does not update with changes in Core Data NSManagedObject. The workaround is to introduce salt, like UUID just to make state change

1
2
3
4
5
6
7
8
9
10
struct NoteRow: View {
let note: Note
let id: UUID
}

List {
ForEach(notes) { note in
NoteRow(note: note, id: UUID())
}
}

Updated at 2020-11-20 03:29:39

How to access bookmark url in macOS

Issue #624

By default the approaches above grant you access while the app remains open. When you quit the app, any folder access you had is lost.

To gain persistent access to a folder even on subsequent launches, we’ll have to take advantage of a system called Security-Scoped Bookmarks.

Add entitlements

Use of app-scoped bookmarks and URLs

1
2
3
4
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<key>com.apple.security.files.bookmarks.app-scope</key>
<true/>

Enabling Security-Scoped Bookmark and URL Access

If you want to provide your sandboxed app with persistent access to file system resources, you must enable security-scoped bookmark and URL access. Security-scoped bookmarks are available starting in macOS v10.7.3.

To add the bookmarks.app-scope or bookmarks.document-scope entitlement, edit the target’s .entitlements property list file using the Xcode property list editor. Use the entitlement keys shown in Table 4-4, depending on which type of access you want. Use a value of for each entitlement you want to enable. You can enable either or both entitlements.

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
func saveBookmark(item: ShortcutItem) {
guard let url = item.fileUrl else { return }
do {
let bookmarkData = try url.bookmarkData(
options: .withSecurityScope,
includingResourceValuesForKeys: nil,
relativeTo: nil
)

item.bookmark = bookmarkData
} catch {
print("Failed to save bookmark data for \(url)", error)
}
}

func loadBookmark(item: ShortcutItem) -> URL? {
guard let data = item.bookmark else { return nil }
do {
var isStale = false
let url = try URL(
resolvingBookmarkData: data,
options: .withSecurityScope,
relativeTo: nil,
bookmarkDataIsStale: &isStale
)
if isStale {
saveBookmark(item: item)
}
return url
} catch {
print("Error resolving bookmark:", error)
return nil
}
}


_ = url.startAccessingSecurityScopedResource()
NSWorkspace.shared.open(url)
url.stopAccessingSecurityScopedResource()
1
2
3
4
5
6
_ = url.startAccessingSecurityScopedResource()
NSWorkspace.shared.selectFile(
url.path,
inFileViewerRootedAtPath: url.deletingLastPathComponent().path
)
url.stopAccessingSecurityScopedResource()

Read more

How to force FetchRequest update in SwiftUI

Issue #623

Listen to context changes notification and change SwiftUI View state

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let changes = [NSDeletedObjectsKey: ids]
NSManagedObjectContext.mergeChanges(
fromRemoteContextSave: changes,
into: [context]
)
try context.save()

struct ListView: View {
@Environment(\.managedObjectContext)
var context

private var didSave = NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave)
@State
private var refreshing: Bool = false

var body: some View {
makeContent()
.onReceive(didSave) { _ in
self.refreshing.toggle()
}
}
}

We need to actually use that State variable for it to have effect

1
2
3
4
5
if refreshing {
Text("")
} else {
Text("")
}

How to batch delete in Core Data

Issue #622

Read Implementing Batch Deletes

If the entities that are being deleted are not loaded into memory, there is no need to update your application after the NSBatchDeleteRequest has been executed. However, if you are deleting objects in the persistence layer and those entities are also in memory, it is important that you notify the application that the objects in memory are stale and need to be refreshed.

To do this, first make sure the resultType of the NSBatchDeleteRequest is set to NSBatchDeleteRequestResultType.resultTypeObjectIDs before the request is executed. When the request has completed successfully, the resulting NSPersistentStoreResult instance that is returned will have an array of NSManagedObjectID instances referenced in the result property. That array of NSManagedObjectID instances can then be used to update one or more NSManagedObjectContext instances.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = Book.fetchRequest()
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
deleteRequest.resultType = .resultTypeObjectIDs

do {
let context = CoreDataManager.shared.container.viewContext
let result = try context.execute(
deleteRequest
)

guard
let deleteResult = result as? NSBatchDeleteResult,
let ids = deleteResult.result as? [NSManagedObjectID]
else { return }

let changes = [NSDeletedObjectsKey: ids]
NSManagedObjectContext.mergeChanges(
fromRemoteContextSave: changes,
into: [context]
)
} catch {
print(error as Any)
}

How to update FetchRequest with predicate in SwiftUI

Issue #621

Make subview that accepts FetchRequest. Trigger search by setting property

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
struct SideView: View {
@Environment(\.managedObjectContext)
var context

@State var search: Search?

var body: some View {
VStack(alignment: .leading) {
SearchView(
onSearch: self.onSearch
)
InsideListView(fetchRequest: makeFetchRequest())
}
}

private func makeFetchRequest() -> FetchRequest<Book> {
let predicate: NSPredicate?
if let search = search {
let textPredicate = NSPredicate(format: "string CONTAINS[cd] %@", search.text)
let appPredicate = NSPredicate(format: "appName == %@", search.app)
let typePredicate = NSPredicate(format: "type == %@", search.type)

var predicates: [NSPredicate] = []
if search.text.count >= 3 {
predicates.append(textPredicate)
}

if search.app != Constants.all {
predicates.append(appPredicate)
}

if search.type != Constants.all {
predicates.append(typePredicate)
}

predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates)
} else {
predicate = nil
}

return FetchRequest<Book>(
entity: Book.entity(),
sortDescriptors: [
NSSortDescriptor(keyPath: \Book.date, ascending: false)
],
predicate: predicate
)
}

private func onSearch(_ search: Search) {
if search.text.count < 3 && search.type != Constants.all && search.app != Constants.all {
self.search = nil
} else {
self.search = search
}
}
}

private struct InsideListView: View {
@Environment(\.managedObjectContext)
var context

var fetchRequest: FetchRequest<Book>

var body: some View {
List(items) {
ForEach
}
}

private var items: FetchedResults<Book> {
fetchRequest.wrappedValue
}
}

How to make TextField focus in SwiftUI for macOS

Issue #620

For NSWindow having levelother than .normal, need to override key and main property to allow TextField to be focusable

1
2
3
4
class FocusWindow: NSWindow {
override var canBecomeKey: Bool { true }
override var canBecomeMain: Bool { true }
}

Furthermore to customize TextField, consider using custom

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

struct MyTextField: NSViewRepresentable {
@Binding var text: String

func makeNSView(context: NSViewRepresentableContext<MyTextField>) -> NSTextField {
let tf = NSTextField()
tf.focusRingType = .none
tf.isBordered = false
tf.drawsBackground = false
tf.delegate = context.coordinator
return tf
}

func updateNSView(_ nsView: NSTextField, context: NSViewRepresentableContext<MyTextField>) {
nsView.stringValue = text
}

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

class Coordinator: NSObject, NSTextFieldDelegate {
let parent: MyTextField
init(parent: MyTextField) {
self.parent = parent
}

func controlTextDidChange(_ obj: Notification) {
let textField = obj.object as! NSTextField
parent.text = textField.stringValue
}
}
}

How to show popover for item in ForEach in SwiftUI

Issue #618

Create custom Binding

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
List {
ForEach(self.items) { (item: item) in
ItemRowView(item: item)
.popover(isPresented: self.makeIsPresented(item: item)) {
ItemDetailView(item: item)
}
}
}

func makeIsPresented(item: Item) -> Binding<Bool> {
return .init(get: {
return self.selectedId == item.id
}, set: { _ in
self.selectedId = nil
})
}

How to make tooltip in SwiftUI for macOS

Issue #617

On macOS 11, we can use .help modifier to add tooltip

1
2
Button()
.help("Click here to open settings")

If you support macOS 10.15, then create empty NSView and use as overlay. Need to updateNSView in case we toggle the state of tooltip

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import SwiftUI

struct Tooltip: NSViewRepresentable {
let tooltip: String

func makeNSView(context: NSViewRepresentableContext<Tooltip>) -> NSView {
let view = NSView()
view.toolTip = tooltip
return view
}

func updateNSView(_ nsView: NSView, context: NSViewRepresentableContext<Tooltip>) {
nsView.toolTip = tooltip
}
}
1
2
3
4
5
6
Button(action: self.onGear) {
Image("gear")
.styleButton()
}
.overlay(Tooltip(tooltip: "Settings"))
.buttonStyle(BorderlessButtonStyle())

Now we can add tooltip as a background. Before I used to add as overlay but that prevents interaction, even with .disabled(true)

1
2
3
4
5
6
Button(action: self.onGear) {
Image("gear")
.styleButton()
.background(ToolTip(tooltip: "Settings"))
}
.buttonStyle(BorderlessButtonStyle())

And we can even make an extension on View to use tooltip easily

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import SwiftUI

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public extension View {
func toolTip(_ text: String) -> some View {
return self
.background(
ToolTip(text)
)
}
}

Button(action: self.onGear) {
Text("Click here")
}
.toolTip("Show statistics")

Updated at 2020-12-12 19:54:53

How to make tab view in SwiftUI

Issue #614

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
struct MyTabView: View {
@EnvironmentObject
var preferenceManager: PreferenceManager

var body: some View {
VOrH(isVertical: preferenceManager.preference.position.isVertical) {
OneTabView(image: "one", text: "One", tab: .one)
OneTabView(image: "two", text: "Two", tab: .two)
OneTabView(image: "three", text: "Three", tab: .three)
Spacer()
}
}
}

struct OneTabView: View {
@EnvironmentObject
var preferenceManager: PreferenceManager

let image: String
let text: String
let tab: Tab

var selected: Bool {
preferenceManager.preference.tab == tab
}

var body: some View {
Button(action: { self.preferenceManager.preference.tab = self.tab }) {
VStack(spacing: 2) {
Image(image)
.renderingMode(selected ? .original : .template)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 20)
Text(text)
.font(.system(.caption, design: .rounded))
.fontWeight(selected ? .semibold : .none)
}
}
.padding(.horizontal, 4)
.buttonStyle(BorderlessButtonStyle())
.frame(width: 60, height: 50)
.background(selected ? R.color.selectedTabBackground : Color.clear)
.cornerRadius(4)
}
}

How to return VStack or HStack in SwiftUI

Issue #613

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
struct VOrH<Content>: View where Content: View {
let isVertical: Bool
let content: () -> Content

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

var body: some View {
makeContent()
}

private func makeContent() -> some View {
if isVertical {
return VStack(spacing: 0) {
content()
}.eraseToAnyView()
} else {
return HStack(spacing: 0) {
content()
}.eraseToAnyView()
}
}
}

How to present NSWindow modally

Issue #612

Use runModal

This method runs a modal event loop for the specified window synchronously. It displays the specified window, makes it key, starts the run loop, and processes events for that window. (You do not need to show the window yourself.) While the app is in that loop, it does not respond to any other events (including mouse, keyboard, or window-close events) unless they are associated with the window. It also does not perform any tasks (such as firing timers) that are not associated with the modal run loop. In other words, this method consumes only enough CPU time to process events and dispatch them to the action methods associated with the modal window.

Specify level in windowDidBecomeKey

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
let controller = SettingsWindowController()
NSApp.runModal(for: controller.window!)

final class SettingsWindowController: NSWindowController, NSWindowDelegate {
init() {
let mainView = SettingsView()

let window = NSWindow(
contentRect: CGRect(x: 0, y: 0, width: 500, height: 300),
styleMask: [.titled, .closable],
backing: .buffered,
defer: false
)
let hosting = NSHostingView(rootView: mainView)
window.contentView = hosting

super.init(window: window)
window.delegate = self
}

func windowDidBecomeKey(_ notification: Notification) {
window?.level = .statusBar
}
func windowWillClose(_ notification: Notification) {
NSApp.stopModal()
}
}

How to use Picker with enum in SwiftUI

Issue #611

1
2
3
4
5
6
7
8
9
10
11
enum WindowPosition: String, CaseIterable {
case left
case top
case bottom
case right
}
Picker(selection: $preference.position, label: Text("Position")) {
ForEach(WindowPosition.allCases, id: \.self) {
Text($0.rawValue)
}
}

How to use visual effect view in NSWindow

Issue #610

Set NSVisualEffectView as contentView of NSWindow, and our main view as subview of it. Remember to set frame or autoresizing mask as non-direct content view does not get full size as the window

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let mainView = MainView()
.environment(\.managedObjectContext, coreDataManager.container.viewContext)

window = NSWindow(
contentRect: .zero,
styleMask: [.fullSizeContentView],
backing: .buffered,
defer: false
)
window.titlebarAppearsTransparent = true
window.center()
window.level = .statusBar
window.setFrameAutosaveName("MyApp")

let visualEffect = NSVisualEffectView()
visualEffect.blendingMode = .behindWindow
visualEffect.state = .active
visualEffect.material = .sidebar

let hosting = NSHostingView(rootView: mainView)
window.contentView = visualEffect
visualEffect.addSubview(hosting)
hosting.autoresizingMask = [.width, .height]

Updated at 2021-01-05 21:13:39

How to animate NSWindow

Issue #609

Use animator proxy and animate parameter

1
2
3
4
5
6
7
8
var rect = window.frame
rect.frame.origin.x = 1000
NSAnimationContext.runAnimationGroup({ context in
context.timingFunction = CAMediaTimingFunction(name: .easeIn)
window.animator().setFrame(rect, display: true, animate: true)
}, completionHandler: {

})

Updated at 2020-11-22 18:26:33

How to find active application in macOS

Issue #608

An NSRunningApplication instance for the current application.

1
NSRunningApplication.current

The running app instance for the app that receives key events.

1
NSWorkspace.shared.frontmostApplication

How to compare for nearly equal in Swift

Issue #607

Implement Equatable and Comparable and use round

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 RGBA: Equatable, Comparable {
let red: CGFloat
let green: CGFloat
let blue: CGFloat
let alpha: CGFloat

init(_ red: CGFloat, _ green: CGFloat, _ blue: CGFloat, _ alpha: CGFloat) {
self.red = red
self.green = green
self.blue = blue
self.alpha = alpha
}

static func round(_ value: CGFloat) -> CGFloat {
(value * 100).rounded() / 100
}

static func == (left: RGBA, right: RGBA) -> Bool {
let r = Self.round
return r(left.red) == r(right.red)
&& r(left.green) == r(right.green)
&& r(left.blue) == r(right.blue)
&& r(left.alpha) == r(right.alpha)
}

static func < (left: RGBA, right: RGBA) -> Bool {
let r = Self.round
return r(left.red) < r(right.red)
&& r(left.green) < r(right.green)
&& r(left.blue) < r(right.blue)
&& r(left.alpha) <= r(right.alpha)
}
}

XCTAssertGreaterThan(backgroundRgba, RGBA(0.57, 0.12, 0.88, 1.0)
XCTAssertLessThanThan(backgroundRgba, RGBA(0.57, 0.12, 0.88, 1.0)