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


Updated at 2020-06-01 02:13:51

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

How to avoid pitfalls in SwiftUI

Issue #602

Identify by unique id

1
2
3
ForEach(store.blogs.enumerated().map({ $0 }), id: \.element.id) { index, blog in

}

##

How to use TabView with enum in SwiftUI

Issue #599

Specify tag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
enum Authentication: Int, Codable {
case key
case certificate
}


TabView(selection: $authentication) {
KeyAuthenticationView()
.tabItem {
Text("🔑 Key")
}
.tag(Authentication.key)
CertificateAuthenticationView()
.tabItem {
Text("📰 Certificate")
}
.tag(Authentication.certificate)
}

Updated at 2021-02-23 00:45:29

How to build SwiftUI style UICollectionView data source in Swift

Issue #598

It’s hard to see any iOS app which don’t use UITableView or UICollectionView, as they are the basic and important foundation to represent data. UICollectionView is very basic to use, yet a bit tedious for common use cases, but if we abstract over it, then it becomes super hard to customize. Every app is unique, and any attempt to wrap around UICollectionView will fail horribly. A sensable approach for a good abstraction is to make it super easy for normal cases, and easy to customize for advanced scenarios.

I’m always interested in how to make UICollectionView easier and fun to write and have curated many open sources here data source. Many of these data source libraries try to come up with totally different namings and complex paradigm which makes it hard to onboard, and many are hard to customize.

In its simplest form, what we want in a UICollectionView data source is cell = f(state), which means our cell representation is just a function of the state. We just want to set model to the cell, the correct cell, in a type safe manner.

Generic data source

The basic is to make a generic data source that sticks with a particular cell

1
2
3
4
5
class DataSource<T>: NSObject {
let items: [T]
let configure: (T, UICollectionViewCell) -> Void
let select: (UICollectionViewCell, IndexPath) -> Void
}

This works for basic usage, and we can create multiple DataSource for each kind of model. The problem is it’s hard to subclass DataSource as generic in Swift and inheritance for ObjcC NSObject don’t work well.

Check for the types

Seeing the problem with generic data source, I’ve tried another approach with Upstream where it’s easier to declare sections and models.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let sections: [Section] = [
Section(
header: Header(model: Model.header("Information"), viewType: HeaderView.self),
items: [
Item(model: Model.avatar(avatarUrl), cellType: AvatarCell.self),
Item(model: Model.name("Thor"), cellType: NameCell.self),
Item(model: Model.location("Asgard"), cellType: NameCell.self)
]
),
Section(
header: Header(model: Model.header("Skills"), viewType: HeaderView.self),
items: [
Item(model: Model.skill("iOS"), cellType: SkillCell.self),
Item(model: Model.skill("Android"), cellType: SkillCell.self)
]
)
]

adapter.reload(sections: sections)

This uses the Adapter pattern and we need to handle AdapterDelegate. To avoid the generic problem, this Adapter store items as Any, so we need to type cast all the time.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
extension ProfileViewController: AdapterDelegate {
func configure(model: Any, view: UIView, indexPath: IndexPath) {
guard let model = model as? Model else {
return
}

switch (model, view) {
case (.avatar(let string), let cell as Avatarcell):
cell.configure(string: string)
case (.name(let name), let cell as NameCell):
cell.configure(string: name)
case (.header(let string), let view as HeaderView):
view.configure(string: string)
default:
break
}
}
}

The benefit is that we can easily subclass this Adapter manager to customize the behaviour, here is how to make accordion

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class AccordionManager<T>: Manager<T> {
private var collapsedSections = Set<Int>()

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return collapsedSections.contains(section)
? 0 : sections[section].items.count
}

func toggle(section: Int) {
if collapsedSections.contains(section) {
collapsedSections.remove(section)
} else {
collapsedSections.insert(section)
}

let indexSet = IndexSet(integer: section)
tableView?.reloadSections(indexSet, with: .automatic)
}
}

SwiftUI

SwiftUI comes in iOS 13 with a very concise and easy to use syntax. SwiftUI has good diffing so we just need to update our models so the whole content will be diffed and rendered again.

1
2
3
4
5
6
7
8
9
10
11
12
var body: some View {
List {
ForEach(blogs) { blog in
VStack {
Text(blog.name)
}
.onTap {
print("cell was tapped")
}
}
}
}

SwiftUI style with diffing

I built DeepDiff before and it was used by many people. Now I’m pleased to introduce Micro which is a SwiftU style with DeepDiff powered so it performs fast diffing whenever state changes.

With Micro we can just use the familiar forEach to declare Cell, and the returned State will tell DataSource to update the UICollectionView.

Every time state is assigned, UICollectionView will be fast diffed and reloaded. The only requirement is that your model should conform to DiffAware with diffId so DeepDiff knows how to diff for changes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let dataSource = DataSource(collectionView: collectionView)
dataSource.state = State {
ForEach(blogs) { blog in
Cell<BlogCell>() { context, cell in
cell.nameLabel.text = blog.name
}
.onSelect { context in
print("cell at index \(context.indexPath.item) is selected")
}
.onSize { context in
CGSize(
width: context.collectionView.frame.size.width,
height: 40
)
}
}
}

DataSource is completely overridable, if you want to customize any methods, just subclass DataSource, override methods and access its state.models

1
2
3
4
5
6
class CustomDataSource: DataSource {
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let blog = state.models[indexPath.item] as? Blog
print(blog)
}
}

Diffable data source in iOS 13

In iOS 13, Apple adds Using Collection View Compositional Layouts and Diffable Data Sources which is very handy.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func makeDataSource() -> UITableViewDiffableDataSource<Section, Contact> {
let reuseIdentifier = cellReuseIdentifier

return UICollectionViewDiffableDataSource(
collectionView: collectionView,
cellProvider: { collectionView, indexPath, blog in
let cell = tableView.dequeueReusableCell(
withIdentifier: reuseIdentifier,
for: indexPath
)

cell.textLabel?.text = blog.name
cell.detailTextLabel?.text = blog.email
return cell
}
)
}

This is iOS 13+ only, and the main components are the cellProvider acting as cellForItemAtIndexPath, and the snapshot for diffing. It also supports section.

1
2
let snapshot = NSDiffableDataSourceSnapshot<Section, Blog>()
dataSource.apply(snapshot, animatingDifferences: animate)

How to make round border in SwiftUI

Issue #597

1
2
3
4
5
6
7
8
TextView(font: R.font.text!, lineCount: nil, text: $text, isFocus: $isFocus)
.padding(8)
.background(R.color.inputBackground)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(isFocus ? R.color.inputBorderFocus : Color.clear, lineWidth: 1)
)

How to add drag and drop in SwiftUI

Issue #594

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 SelectFileView: View {
let buttonTitle: String
@State var isDrop: Bool = false

var body: some View {
VStack(alignment: .leading) {
Button(action: {}) {
Text(buttonTitle)
}
.buttonStyle(ActionButtonStyle())
.offset(x: -16)
Text("Alternatively, you can drag and drop file here")
.font(.footnote)
.foregroundColor(Color.gray)
}
.border(isDrop ? R.color.separator : Color.clear)
.onDrop(of: [Constants.urlFileType], delegate: self)
.padding(.bottom, 32)
}
}

extension SelectFileView: DropDelegate {
func dropEntered(info: DropInfo) {
self.isDrop = true
}

func dropExited(info: DropInfo) {
self.isDrop = false
}

func performDrop(info: DropInfo) -> Bool {
guard
let itemProvider = info.itemProviders(for: [Constants.urlFileType]).first
else { return false }

itemProvider.loadItem(forTypeIdentifier: Constants.urlFileType, options: nil) { item, error in
guard
let data = item as? Data,
let url = URL(dataRepresentation: data, relativeTo: nil)
else { return }

}

return true
}
}

Updated at 2020-07-09 01:45:55

How to make radio button group in SwiftUI

Issue #592

Use picker with Radio style

Hard to customize

1
2
3
4
Picker(selection: Binding<Bool>.constant(true), label: EmptyView()) {
Text("Production").tag(0)
Text("Sandbox").tag(1)
}.pickerStyle(RadioGroupPickerStyle())

Use custom view

Use contentShape to make whole button tappable.
Make custom Binding for our enum

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
struct EnvironmentView: View {
@Binding var input: Input

var body: some View {
VStack(alignment: .leading) {
RadioButton(text: "Production", isOn: binding(for: .production))
RadioButton(text: "Sandbox", isOn: binding(for: .sandbox))
}
}

private func binding(for environment: Input.Environment) -> Binding<Bool> {
Binding<Bool>(
get: { self.input.environment == environment },
set: { flag in
if flag {
self.input.environment = environment
}
}
)
}
}

struct RadioButton: View {
let text: String
@Binding var isOn: Bool

var body: some View {
Button(action: {
self.isOn.toggle()
}) {
HStack(alignment: .top) {
Circle()
.fill(isOn ? R.color.primary : Color.clear)
.overlay(Circle().stroke(R.color.primary))
.frame(width: 18, height: 18)
Text(text)
.foregroundColor(R.color.text)
}
.contentShape(Rectangle())
}
.buttonStyle(RadioButtonStyle())
}
}

struct RadioButtonStyle: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.padding(.vertical, 4)
.padding(.horizontal, 8)
.border(SeparatorShapeStyle(), width: 0)
.background(Color.clear)
}
}

How to make borderless material NSTextField in SwiftUI for macOS

Issue #590

Use custom NSTextField as it is hard to customize TextFieldStyle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import SwiftUI

struct MaterialTextField: View {
let placeholder: String
@Binding var text: String
@State var isFocus: Bool = false

var body: some View {
VStack(alignment: .leading, spacing: 0) {
BorderlessTextField(placeholder: placeholder, text: $text, isFocus: $isFocus)
.frame(maxHeight: 40)
Rectangle()
.foregroundColor(isFocus ? R.color.separatorFocus : R.color.separator)
.frame(height: isFocus ? 2 : 1)
}
}
}

class FocusAwareTextField: NSTextField {
var onFocusChange: (Bool) -> Void = { _ in }

override func becomeFirstResponder() -> Bool {
let textView = window?.fieldEditor(true, for: nil) as? NSTextView
textView?.insertionPointColor = R.nsColor.action
onFocusChange(true)
return super.becomeFirstResponder()
}
}

struct BorderlessTextField: NSViewRepresentable {
let placeholder: String
@Binding var text: String
@Binding var isFocus: Bool

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

func makeNSView(context: Context) -> NSTextField {
let textField = FocusAwareTextField()
textField.placeholderAttributedString = NSAttributedString(
string: placeholder,
attributes: [
NSAttributedString.Key.foregroundColor: R.nsColor.placeholder
]
)
textField.isBordered = false
textField.delegate = context.coordinator
textField.backgroundColor = NSColor.clear
textField.textColor = R.nsColor.text
textField.font = R.font.text
textField.focusRingType = .none
textField.onFocusChange = { isFocus in
self.isFocus = isFocus
}

return textField
}

func updateNSView(_ nsView: NSTextField, context: Context) {
nsView.stringValue = text
}

class Coordinator: NSObject, NSTextFieldDelegate {
let parent: BorderlessTextField

init(_ textField: BorderlessTextField) {
self.parent = textField
}

func controlTextDidEndEditing(_ obj: Notification) {
self.parent.isFocus = false
}

func controlTextDidChange(_ obj: Notification) {
guard let textField = obj.object as? NSTextField else { return }
self.parent.text = textField.stringValue
}
}
}

How to show localized text in SwiftUI

Issue #533

1
2
3
4
5
6
7
8
9
10
11
12
struct ContentView: View {
@Environment(\.locale) var locale: Locale

var body: some View {
VStack {
Text(LocalizedStringKey("hello"))
.font(.largeTitle)
Text(flag(from: locale.regionCode!))
.font(.largeTitle)
}
}
}

How to use ForEach with ScrollView in SwiftUI

Issue #517

Use ScrollView -> VStack -> ForEach -> Content

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct SearchScreen: View {
@State var searchObjects: [SearchObject] = [
SearchObject(name: "By name", search: { CountryManager.shared.search(byName: $0) }),
SearchObject(name: "By calling code", search: { CountryManager.shared.search(byCallingCode: $0) }),
SearchObject(name: "By domain", search: { CountryManager.shared.search(byDomain: $0) }),
SearchObject(name: "By language", search: { CountryManager.shared.search(byLanguage: $0) })
]

var body: some View {
ScrollView {
VStack(alignment: .leading) {
ForEach(searchObjects.enumerated().map({ $0 }), id: \.element.name, content: { index, searchObject in
VStack(alignment: .leading) {
Text(searchObject.name)
.styleLabel()
TextField(searchObject.textFieldName, text: self.$searchObjects[index].text)
.styleTitle()
self.makeButton(searchObject: self.searchObjects[index])
}
})
}
}
}
}

How to modify data inside array in SwiftUI

Issue #516

Suppose we have an array of SearchObject, and user can enter search query into text property.

1
2
3
4
5
6
7
8
9
10
class SearchObject: ObservableObject {
let name: String
let search: (String) -> [Country]
var text: String = ""

init(name: String, search: @escaping (String) -> [Country]) {
self.name = name
self.search = search
}
}

Although SearchObject is class, when we use ForEach, the changes to passed object won’t be reflected in our array and there is no reload trigger, we need to point to object in array directly, like

1
self.$searchObjects[index].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
struct SearchScreen: View {
@State var searchObjects: [SearchObject] = [
SearchObject(name: "By name", search: { CountryManager.shared.search(byName: $0) }),
SearchObject(name: "By calling code", search: { CountryManager.shared.search(byCallingCode: $0) }),
SearchObject(name: "By domain", search: { CountryManager.shared.search(byDomain: $0) }),
SearchObject(name: "By language", search: { CountryManager.shared.search(byLanguage: $0) })
]

var body: some View {
ScrollView {
VStack(alignment: .leading) {
ForEach(searchObjects.enumerated().map({ $0 }), id: \.element.name, content: { index, searchObject in
VStack(alignment: .leading) {
Text(searchObject.name)
.styleLabel()
TextField(searchObject.textFieldName, text: self.$searchObjects[index].text)
.styleTitle()
self.makeButton(searchObject: self.searchObjects[index])
}
})
}
}
}
}

How to use index in SwiftUI list

Issue #515

Use enumerated and id: \.element.name

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct CountriesView: View {
let countries: [Country]

var body: some View {
let withIndex = countries.enumerated().map({ $0 })

return List(withIndex, id: \.element.name) { index, country in
NavigationLink(
destination: CountryView(country: country),
label: {
VStack(alignment: .leading) {
Text(country.name)
.styleMultiline()
}
.paddingVertically()
}
)
}
}
}

How to use objectWillChange in Combine

Issue #513

A publisher that emits before the object has changed

Use workaround DispatchQueue to wait another run loop to access newValue

1
2
3
4
5
6
7
8
9
.onReceive(store.objectWillChange, perform: {
DispatchQueue.main.async {
self.reload()
}
})

func reload() {
self.isFavorite = store.isFavorite(country: country)
}

Read more

How to show list with section in SwiftUI

Issue #511

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 CountriesView: View {
let groups: [Group]

init(countries: [Country]) {
self.groups = CountryManager.shared.groups(countries: countries)
}

var body: some View {
List {
ForEach(groups) { group in
Section(
header:
Text(group.initial)
.foregroundColor(Color.yellow)
.styleTitle(),
content: {
ForEach(group.countries) { country in
CountryRow(country: country)
}
}
)
}
}
}
}

How to make full width list row in SwiftUI

Issue #508

We need to use frame(minWidth: 0, maxWidth: .infinity, alignment: .leading). Note that order is important, and padding should be first, and background after frame to apply color to the entire frame

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
struct BooksScreen: View {
@ObservedObject var viewModel: BooksViewModel

var body: some View {
List {
ForEach(viewModel.books) { book in
RowView(vault: book)
}
}
.listStyle(GroupedListStyle())
}
}

private struct RowView: View {
let book: Book

var body: some View {
VStack(alignment: .leading) {
Text(book.name)
.foregroundColor(.white)
.font(.headline)
Text(book.text)
.foregroundColor(.white)
.font(.subheadline)
}
.padding()
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
.background(Color(hex: book.color))
.cornerRadius(8)
}
}

How to make full screen TabView in SwiftUI

Issue #507

View extends to the bottom, but not to the notch. We need to add .edgesIgnoringSafeArea(.top) to our TabView to tell TabView to extend all the way to the top.

Note that if we use edgesIgnoringSafeArea(.all) then TabView ‘s bar will be dragged very down and broken.

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
struct MainScreen: View {
init() {
UITabBar.appearance().backgroundColor = R.color.barBackground
UITableView.appearance().backgroundColor = .clear
UITableViewCell.appearance().backgroundColor = .clear
UITableView.appearance().tableFooterView = UIView()
}

var body: some View {
ZStack {
R.color.background
.edgesIgnoringSafeArea(.all)

TabView {
PersonalScreen()
.tabItem({
Image(sfSymbol: .bagFill)
Text("Personal")
.styleTabBarItem()
})
CloudScreen()
.tabItem({
Image(sfSymbol: .cloudFill)
Text("Cloud")
.styleTabBarItem()
})
SettingsScreen()
.tabItem({
Image(sfSymbol: .gear)
Text("Settings")
.styleTabBarItem()
})
}
.edgesIgnoringSafeArea(.top)
}
}
}

How to make simple Redux for SwiftUI

Issue #502

Mutation is used to mutate state synchronously. Action is like intent, either from app or from user action. Action maps to Mutation in form of Publisher to work with async action, similar to redux-observable

AnyReducer is a type erasure that takes the reduce function

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

public protocol Reducer {
associatedtype State
associatedtype Mutation
func reduce(state: State, mutation: Mutation) -> State
}

public struct AnyReducer<State, Mutation> {
public let reduce: (State, Mutation) -> State
public init<R: Reducer>(reducer: R) where R.State == State, R.Mutation == Mutation {
self.reduce = reducer.reduce
}
}

public protocol Action {
associatedtype Mutation
func toMutation() -> AnyPublisher<Mutation, Never>
}

public final class Store<State, Mutation>: ObservableObject {
@Published public private(set) var state: State
public let reducer: AnyReducer<State, Mutation>
public private(set) var cancellables = Set<AnyCancellable>()

public init(initialState: State, reducer: AnyReducer<State, Mutation>) {
self.state = initialState
self.reducer = reducer
}

public func send<A: Action>(action: A) where A.Mutation == Mutation {
action
.toMutation()
.receive(on: DispatchQueue.main)
.sink(receiveValue: update(mutation:))
.store(in: &cancellables)
}

public func update(mutation: Mutation) {
self.state = reducer.reduce(state, mutation)
}
}

To use, conform to all the protocols. Also make typelias AppStore in order to easy specify type in SwiftUI View

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

typealias AppStore = Store<AppState, AppMutation>

let appStore: AppStore = AppStore(
initialState: AppState(),
reducer: appReducer
)

struct AppState: Codable {
var hasShownOnboaring = false
}

struct AppReducer: Reducer {
func reduce(state: AppState, mutation: AppMutation) -> AppState {
var state = state
switch mutation {
case .finishOnboarding:
state.hasShownOnboaring = true
@unknown default:
break
}

return state
}
}

enum AppMutation {
case finishOnboarding
}

Use in SwiftUI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct RootScreen: View {
@EnvironmentObject var store: AppStore

var body: some View {
if store.state.hasShownOnboaring {
return Text("Welcome")
.eraseToAnyView()
} else {
return OnboardingScreen()
.eraseToAnyView()
}
}
}

struct OnboardingScreen: View {
@EnvironmentObject var store: AppStore

private func done() {
store.send(action: AppAction.finishOnboarding)
}
}

Reference