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 make simple filter menu in css

Issue #643

Use material icons

1
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
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
div#filter-container {
display: flex;
align-items: center;
justify-content: center;
margin-top: 10%;
height: 60px;
}

div#filter-items {
display: inline-flex;

background-color: #fff;
box-shadow: 0 0 1px 0 rgba(52, 46, 173, 0.25), 0 15px 30px 0 rgba(52, 46, 173, 0.1);
border-radius: 12px;
overflow: hidden;
padding: 10px;
}

a.filter-item {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100px;
text-decoration: none;
padding: 10px;
}

a.filter-item:hover {
background-color: rgb(239, 240, 241);
border-radius: 10px;
}

a.filter-item:active {
transform: scale(0.9);
}

a.filter-item.selected {
background-color: rgb(239, 240, 241);
border-radius: 10px;
}

span.material-icons {
font-family: "Material Icons";
display: block;
margin-bottom: 4px;
font-size: 26px;
color: mix(#fff, #342ead, 60%);
transition: 0.25s ease;
}

span.name {
display: block;
font-size: 13px;
color: mix(#fff, #342ead, 70%);
transition: 0.25s ease;
font-family: 'Open Sans', sans-serif;
font-weight: 500;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
filters.forEach((filter) => {
const item = document.createElement('a')
item.href = '#'
item.className = 'filter-item'
container.appendChild(item)

const icon = document.createElement('span')
icon.className = 'material-icons'
icon.innerText = filter.icon
item.appendChild(icon)

const name = document.createElement('span')
name.className = 'name'
name.innerText = filter.name
item.appendChild(name)

item.onclick = () => {
handleFilterClick(item, filter)
}
})

How to make simple grid gallery in css

Issue #642

Specify container with flex-wrap and justify-content, and item with float: left

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
div.cards {
display: flex;
justify-content: center;
flex-direction: row;
flex-wrap: wrap;
margin-top: 10%;
}

div.card {
overflow: hidden;
float: left;
width: 220px;
color: #232330;
text-align: center;
border-radius: 10px;
border-color: silver;
box-shadow: 1px 8px 8px rgba(10, 10, 10, 0.2);
font-family: 'Open Sans', sans-serif;
margin: 16px;
transition: all .2s ease-in-out;
}

div.card:hover {
transform: scale(1.2);
}

How to add independent page in hexo

Issue #641

Create a new page

1
hexo new page mydemo

Remove index.md and create index.html, you can reference external css and js in this index.html. Hexo has hexo new page mydemo --slug but it does not support page hierarchy

Specify no layout so it is independent page.

1
2
3
---
layout: false
---

How to use async function as parameter in TypeScript

Issue #640

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
async function useCache(
collectionName: string,
key: string,
response: functions.Response<any>,
fetch: () => Promise<any>
): Promise<any> {
const existing = await db.collection(collectionName).doc(key).get()
if (existing.exists) {
response.send(existing.data())
return
}

const object = await fetch()
const json = Object.assign({}, object)
await db.collection(collectionName).doc(key).set(json)
response.send(object)
}
1
2
3
4
5
6
7
8
9
useCache(
"books",
key,
response,
async () => {
const service = new Service()
return await service.doSomething(key)
}
)

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

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

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