How to use CoreData safely

Issue #686

I now use Core Data more often now. Here is how I usually use it, for example in Push Hero

From iOS 10 and macOS 10.12, NSPersistentContainer that simplifies Core Data setup quite a lot. I usually use 1 NSPersistentContainer and its viewContext together with newBackgroundContext attached to that NSPersistentContainer

In Core Data, each context has a queue, except for viewContext using the DispatchQueue.main, and each NSManagedObject retrieved from 1 context is supposed to use within that context queue only, except for objectId property.

Although NSManagedObject subclasses from NSObject, it has a lot of other constraints that we need to be aware of. So it’s safe to treat Core Data as a cache layer, and use our own model on top of it. I usually perform operations on background context to avoid main thread blocking, and automaticallyMergesChangesFromParent handles merge changes automatically for us.

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
extension SendHistoryItem {
func toCoreData(context: NSManagedObjectContext) {
context.perform {
let cd = CDSendHistoryItem(context: context)
}
}
}

extension CDSendHistoryItem {
func toModel() throws -> SendHistoryItem {

}
}

final class CoreDataManager {
private var backgroundContext: NSManagedObjectContext?

init() {
self.backgroundContext = self.persistentContainer.newBackgroundContext()
}

lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "PushHero")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error {
print(error)
}
})
return container
}()

func load(request: NSFetchRequest<CDSendHistoryItem>, completion: @escaping ([SendHistoryItem]) -> Void) {
guard let context = CoreDataManager.shared.backgroundContext else { return }
context.perform {
do {
let cdItems = try request.execute()
let items = cdItems.compactMap({ try? $0.toModel() })
completion(items)
} catch {
completion([])
}
}
}

func save(items: [SendHistoryItem]) {
guard let context = backgroundContext else {
return
}

context.perform {
items.forEach {
let _ = $0.toCoreData(context: context)
}
do {
try context.save()
} catch {
print(error)
}
}
}
}

How to pass ObservedObject as parameter in SwiftUI

Issue #685

Since we have custom init in ChildView to manually set a State, we need to pass ObservedObject. In the ParentView, use underscore _ to access property wrapper type.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct ChildView: View {
@ObservedObject
var store: Store

@State
private var selectedTask: AnyTask

init(store: ObservedObject<Store>) {
_selectedTask = State(initialValue: tasks.first!)
_store = store
}
}

struct ParentView: View {
@ObservedObject
var store: Store

var body: some View {
ChildView(store: _store)
}

How to do equal width in SwiftUI

Issue #684

In SwiftUI, specifying maxWidth as .infinity means taking the whole available width of the container.
If many children ask for max width, then they will be divided equally.
This is similar to weight in LinearLayout in Android or css flex-grow property.

The same applies in vertical direct also.

width

How to style multiline Text in SwiftUI for macOS

Issue #681

Only need to specify fixedSize on text to preserve ideal height.

The maximum number of lines is 1 if the value is less than 1. If the value is nil, the text uses as many lines as required. The default is nil.

1
2
3
Text(longText)
.lineLimit(nil) // No need
.fixedSize(horizontal: false, vertical: true)

If the Text is inside a row in a List, fixedSize causes the row to be in middle of the List, workaround is to use ScrollView and vertical StackView.

Sometimes for Text to properly size itself, specify an explicit frame width

How to clear List background color in SwiftUI for macOS

Issue #680

For List in SwiftUI for macOS, it has default background color because of the enclosing NSScrollView via NSTableView that List uses under the hood. Using listRowBackground also gives no effect

The solution is to use a library like SwiftUI-Introspect

1
2
3
4
5
6
7
8
9
10
import Introspect

extension List {
func removeBackground() -> some View {
return introspectTableView { tableView in
tableView.backgroundColor = .clear
tableView.enclosingScrollView!.drawsBackground = false
}
}
}

then

1
2
3
4
5
6
List {
ForEach(items) { item in
// view here
}
}
.removeBackground()

Or we can add extension on NSTableView to alter its content when it moves to superview

1
2
3
4
5
6
7
8
extension NSTableView {
open override func viewDidMoveToWindow() {
super.viewDidMoveToWindow()

backgroundColor = NSColor.clear
enclosingScrollView!.drawsBackground = false
}
}

This works OK for me on macOS 10.15.5, 10.15.7 and macOS 10.11 beta. But it was reported crash during review on macOS 10.15.6

The app launches briefly and then quits without error message.

After inspecting crash log, it is because of viewDidMoveToWindow. So it’s wise not to mess with NSTableView for now

1
2
3
4
5
Thread 0 Crashed:: Dispatch queue: com.apple.main-thread
0 com.onmyway133.PushHero 0x0000000104770b0a @objc NSTableView.viewDidMoveToWindow() (in Push Hero) (<compiler-generated>:296)
1 com.apple.AppKit 0x00007fff2d8638ea -[NSView _setWindow:] + 2416
2 com.apple.AppKit 0x00007fff2d8844ea -[NSControl _setWindow:] + 158
3 com.apple.AppKit 0x00007fff2d946ace -[NSTableView _setWindow:] + 306

How to avoid reduced opacity when hiding view with animation in SwiftUI

Issue #679

While redesigning UI for my app Push Hero, I ended up with an accordion style to toggle section.

Screenshot 2020-10-01 at 06 58 33

It worked great so far, but after 1 collapsing, all image and text views have reduced opacity. This does not happen for other elements like dropdown button or text.

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
extension View {
func sectionBackground(_ title: String, _ shows: Binding<Bool>) -> some View {
VStack(alignment: .leading) {
HStack {
Text(title.uppercased())
Spacer()
if shows != nil {
SmallButton(
imageName: "downArrow",
tooltip: shows!.wrappedValue ? "Collapse" : "Expand",
action: {
withAnimation(.easeInOut) {
shows!.wrappedValue.toggle()
}
}
)
.rotationEffect(.radians(shows!.wrappedValue ? .pi : 0))
}
}

if shows.wrappedValue {
self
}
}
}
}

The culprit is that withAnimation, it seems to apply opacity effect. So the workaround is to disable animation wrappedValue, or to tweak transition so that there’s no opacity adjustment.

1
2
3
if shows.wrappedValue {
self.transition(AnyTransition.identity)
}

How to dynamically add items to VStack from list in SwiftUI

Issue #678

Use enumerated to get index so we can assign to item in list. Here is how I show list of device tokens in my app Push Hero

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private var textViews: some View {
let withIndex = input.deviceTokens.enumerated().map({ $0 })
let binding: (Int, Input.DeviceToken) -> Binding<String> = { index, token in
Binding<String>(
get: { token.token },
set: { self.input.deviceTokens[index].token = $0 }
)
}

return VStack {
ForEach(withIndex, id: \.element.id) { index, token in
return DeviceTokenTextView(text: binding(index, token))
}
}
}

How to unwrap Binding with Optional in SwiftUI

Issue #677

The quick way to add new properties without breaking current saved Codable is to declare them as optional. For example if you use EasyStash library to save and load Codable models.

1
2
3
4
5
6
7
import SwiftUI

struct Input: Codable {
var bundleId: String = ""

// New props
var notificationId: String?

This new property when using dollar syntax $input.notificationId turn into Binding with optional Binding<Strting?> which is incompatible in SwiftUI when we use Binding.

1
2
3
4
struct MaterialTextField: View {
let placeholder: String
@Binding var text: String
}

The solution here is write an extension that converts Binding<String?> to Binding<String>

1
2
3
4
5
6
7
8
9
10
11
12
extension Binding where Value == String? {
func toNonOptional() -> Binding<String> {
return Binding<String>(
get: {
return self.wrappedValue ?? ""
},
set: {
self.wrappedValue = $0
}
)
}
}

so we can use them as normal

1
MaterialTextField(text: $input.notificationId.toNonOptional())

How to make custom toggle in SwiftUI

Issue #676

I’ve used the default SwiftUI to achieve the 2 tab views in SwiftUI. It adds a default box around the content and also opinionated paddings. For now on light mode on macOS, the unselected tab has wrong colors.

The way to solve this is to come up with a custom toggle, that we can style and align the way we want. Here is how I did for my app Push Hero

Using a Text instead of Button here gives me default static text look.

Screenshot 2020-09-29 at 06 52 33
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
struct AuthenticationView: View {
@Binding var input: Input

var body: some View {
toggle
}

var toggle: some View {
VStack(alignment: .leading) {
HStack {
HStack(spacing: 0) {
Text("🔑 Key")
.style(selected: input.authentication == .key)
.onTapGesture {
self.input.authentication = .key
}

Text("📰 Certificate")
.style(selected: input.authentication == .certificate)
.onTapGesture {
self.input.authentication = .certificate
}
}
.padding(3)
.background(Color.white.opacity(0.2))
.cornerRadius(6)

Spacer()
}

choose
Spacer()
}
}

var choose: AnyView {
switch input.authentication {
case .key:
return KeyAuthenticationView().erase()
case .certificate:
return CertificateAuthenticationView().erase()
}
}
}

private extension Text {
func style(selected: Bool) -> some View {
return self
.padding(.vertical, 3)
.padding(.horizontal, 4)
.background(selected ? Color.white.opacity(0.5) : Color.clear)
.cornerRadius(6)
}
}

How to use Binding in function in Swift

Issue #675

Use wrappedValue to get the underlying value that Binding contains

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
extension View {
func addOverlay(shows: Binding<Bool>) -> some View {
HStack {
self
Spacer()
}
.overlay(
HStack {
Spacer()
SmallButton(
imageName: "downArrow",
tooltip: shows.wrappedValue ? "Collapse" : "Expand",
action: {
shows.wrappedValue.toggle()
}
)
.rotationEffect(.radians(shows.wrappedValue ? .pi : 0))
}
)
}
}

How to use HSplitView to define 3 panes view in SwiftUI for macOS

Issue #674

Specify minWidth to ensure miminum width, and use .layoutPriority(1) for the most important pane.

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

struct MainView: View {
@EnvironmentObject var store: Store

var body: some View {
HSplitView {
LeftPane()
.padding()
.frame(minWidth: 200, maxWidth: 500)
MiddlePane(store: store)
.padding()
.frame(minWidth: 500)
.layoutPriority(1)
RightPane()
.padding()
.frame(minWidth: 300)
}
.background(R.color.background)
}
}

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 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 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 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 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 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 edit selected item in list in SwiftUI

Issue #605

I use custom TextView in a master detail application.

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

struct TextView: NSViewRepresentable {
@Binding var text: String

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

func makeNSView(context: Context) -> NSTextView {
let textView = NSTextView()
textView.delegate = context.coordinator
return textView
}

func updateNSView(_ nsView: NSTextView, context: Context) {
guard nsView.string != text else { return }
nsView.string = text
}

class Coordinator: NSObject, NSTextViewDelegate {
let parent: TextView

init(_ textView: TextView) {
self.parent = textView
}

func textDidChange(_ notification: Notification) {
guard let textView = notification.object as? NSTextView else { return }
self.parent.text = textView.string
}
}
}

No matter which item user selects, textView always updates the first one

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
struct Book {
var name: String = ""
}

class Store: ObservableObject {
@Published var books: [Book] = []
}

struct MainView: View {
@EnvironmentObject var store: Store

var body: some View {
List {
ForEach(store.books.enumerated().map({ $0 }), id: \.element.id) { index, book in {
Text(book.name)
.onTapGesture {
self.store.selectedIndex = index
}
}
}

HStack {
TextView($store.books[store.selectedIndex].name)
}
}
}

The fix is to pass selected object instead of using subscript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct MainView: View {
@EnvironmentObject var store: Store

var body: some View {
List {
ForEach(store.books.enumerated().map({ $0 }), id: \.element.id) { index, book in {
Text(book.name)
.onTapGesture {
self.store.selectedBook = self.store.books[index]
}
}
}

HStack {
TextView($store.selectedBook.name)
}
}
}

And we need to save selectedBook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Store: ObservableObject {
@Published var books: [Book] = []

@Published var selectedBook: Book = Book(name: "") {
didSet {
saveSelected()
}
}

func saveSelected() {
guard let index = self.books.firstIndex(where: { $0.id == selectedBook.id }) else {
return
}

books[index] = selectedBook
}
}

Read more

How to log in SwiftUI

Issue #604

I see that the modifier needs to do something on the content, otherwise it is not getting called!
This logs on the modifier, when the View is created. A View won’t be recreated unless necessary

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct LogModifier: ViewModifier {
let text: String
func body(content: Content) -> some View {
print(text)
return content
.onAppear {}
}
}

extension View {
func log(_ text: String) -> some View {
self.modifier(LogModifier(text: text))
}
}
1
2
3
4
VStack {
Text("")
.log("a text")
}

Another simpler way is to make an extension

1
2
3
4
5
6
extension View {
func log(_ any: Any) -> Self {
print("\(any)")
return self
}
}