How to make switch statement in SwiftUI

Issue #656

Lately I’ve been challenging myself to declare switch statement in SwiftUI, or in a more generalized way, execute any anonymous function that can return a View

Use Switch and Case views

Note that this approach does not work yet, as TupeView should support variadic number of contents, and also T.RawValue needs to conform to Equatable in order to check the cases.

Also in Switch statement, Content can’t be inferred

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
struct Case<T: RawRepresentable, Content: View>: View {
let value: T
let content: Content

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

var body: some View {
content
}
}

struct Switch<T: RawRepresentable, Content: View>: View {
let value: T
let cases: TupleView<Case<T, Content>>

init(_ value: T, @ViewBuilder cases: () -> TupleView<Case<T, Content>>) {
self.value = value
self.cases = cases()
}

var body: some View {
makeBody()
}

private func makeBody() -> some View {
// TODO: Logic here
cases
}
}

struct UseSwitchStatement {
let animal: Animal = .cat

var body: some View {
VStack {
Switch(animal) {
Case(Animal.cat) {
Text("cat")
}
Case(Animal.dog) {
Text("dog")
}
Case(Animal.mouse) {
Text("mouse")
}
}
}
}
}

Use MakeView

Another solution is to use a MakeView view, this is more generic as it can execute any functions

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
enum Animal: String {
case cat
case dog
case mouse
}

struct MakeView: View {
let make: () -> AnyView

var body: some View {
make()
}
}

struct UseMakeView: View {
let animal: Animal = .cat

var body: some View {
MakeView {
switch self.animal {
case .cat:
return Text("cat").erase()
case .dog:
return Text("dog").erase()
case .mouse:
return Text("mouse").erase()
}
}
}
}

How to test DispatchQueue in Swift

Issue #646

Sync the DispatchQueue

Pass DispatchQueue and call queue.sync to sync all async works before asserting

Use mock

Use DispatchQueueType and in mock, call the work immediately

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

public protocol DispatchQueueType {
func async(execute work: @escaping @convention(block) () -> Void)
}

extension DispatchQueue: DispatchQueueType {
public func async(execute work: @escaping @convention(block) () -> Void) {
async(group: nil, qos: .unspecified, flags: [], execute: work)
}
}

final class MockDispatchQueue: DispatchQueueType {
func async(execute work: @escaping @convention(block) () -> Void) {
work()
}
}

How to assert asynchronously in XCTest

Issue #644

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import XCTest

extension XCTestCase {
/// Asynchronously assertion
func XCTAssertWait(
timeout: TimeInterval = 1,
_ expression: @escaping () -> Void,
_: String = "",
file _: StaticString = #file,
line _: UInt = #line
) {
let expectation = self.expectation(description: #function)
DispatchQueue.main.asyncAfter(deadline: .now() + timeout) {
expression()
expectation.fulfill()
}

let waiter = XCTWaiter()
XCTAssertTrue(waiter.wait(for: [expectation], timeout: timeout + 1) == .completed)
}
}

How to format percent in Swift

Issue #639

Never use String(format: "%.2f %%", 1.2 because each region can have different separator and placement of percent sign.

Use NumberFormatter instead

1
2
3
4
5
let formatter = NumberFormatter()
formatter.numberStyle = .percent
formatter.minimumIntegerDigits = 1
formatter.maximumIntegerDigits = 3
formatter.maximumFractionDigits = 2
1
2
3
4
5
formatter.locale = Locale(identifier: "en_US")
formatter.string(from: NSDecimalNumber(decimal: 1.2 / 100)) // 0.12%

formatter.locale = Locale(identifier: "nb_NO")
formatter.string(from: NSDecimalNumber(decimal: 1.2 / 100)) // 0,12 %

Note that the space created by NumberFormatter is a non breakable space \u{00a0}, which can be created by Alt Space. This non breakable space is useful in UILabel when you want the whole word to stick together

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 declare commands in Xcode extenstions

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

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

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

Sometimes it’s better to add overlay tooltip to Image inside Button to avoid blocking

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

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 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 = .appearanceBased

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

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

})

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)

How to conform to Hashable for class in Swift

Issue #606

Use ObjectIdentifier

A unique identifier for a class instance or metatype.

1
2
3
4
5
6
7
8
9
final class Worker: Hashable {
static func == (lhs: Worker, rhs: Worker) -> Bool {
return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
}

func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(self))
}
}