How to conform to Hashable for class in Swift

Issue #606

Use ObjectIdentifier

A unique identifier for a class instance or metatype.

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

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

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 mask with UILabel

Issue #603

Need to set correct frame for mask layer or UILabel, as it is relative to the coordinate of the view to be masked

1
2
3
4
5
6
7
8
9
let aView = UIView(frame: .init(x: 100, y: 110, width: 200, height: 100))

let textLayer = CATextLayer()
textLayer.foregroundColor = UIColor.white.cgColor
textLayer.string = "Hello world"
textLayer.font = UIFont.preferredFont(forTextStyle: .largeTitle)
textLayer.frame = aView.bounds

aView.layer.mask = textLayer

Use sizeToFit to ensure frame for UILabel

1
2
3
4
5
6
7
8
9
let label = UILabel()
label.frame.origin = CGPoint(x: 80, y: 80)

label.textColor = UIColor.black
label.font = UIFont.preferredFont(forTextStyle: .largeTitle)
label.text = "ABC"
label.sizeToFit()

aView.mask = label

Change bounds.origin

1
2
label.frame.origin = CGPoint(x: 50, y: 50)
aView.bounds.origin = label.frame.origin

Adding label to view hierarchy seems to remove masking effect. Need to set mask later

1
2
3
view.addSubview(aView)
view.addSubview(label)
aView.mask = label

Can’t add overlayView to UILabel and use UILabel as mask, cause cycler CALayer

After using UILabel as mask, its superview is nil

1
2
aView.mask = label
label.superview == nil

Mask with snapshot from UILabel. Need to set correct frame for aView and maskLayer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let maskLayer = CALayer()
maskLayer.contents = label.makeScreenshot().cgImage
maskLayer.contentsGravity = .resizeAspect

aView.frame = label.bounds
maskLayer.frame = aView.bounds
aView.layer.mask = maskLayer
label.addSubview(aView)

extension UIView {
func makeScreenshot() -> UIImage {
let renderer = UIGraphicsImageRenderer(bounds: self.bounds)
return renderer.image { (context) in
self.layer.render(in: context.cgContext)
}
}
}

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 sync multiple CAAnimation

Issue #600

Use same CACurrentMediaTime

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
final class AnimationSyncer {
static let now = CACurrentMediaTime()

func makeAnimation() -> CABasicAnimation {
let animation = CABasicAnimation(keyPath: "opacity")
animation.fillMode = .forwards
animation.fromValue = 0
animation.toValue = 1
animation.repeatCount = .infinity
animation.duration = 2
animation.beginTime = Self.now
animation.autoreverses = true
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
return animation
}
}

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 mock in Swift

Issue #596

Unavailable init

1
2
3
4
5
UNUserNotificationCenter.current().getNotificationSettings(completionHandler: { (settings: UNNotificationSettings) in
let status: UNAuthorizationStatus = .authorized
settings.setValue(status.rawValue, forKey: "authorizationStatus")
completionHandler(settings)
})

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 make focusable NSTextField in macOS

Issue #589

Use onTapGesture

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

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

var body: some View {
FocusTextField(text: $text, placeholder: placeholder, isFocus: $isFocus)
.padding()
.cornerRadius(4)
.overlay(
RoundedRectangle(cornerRadius: 4)
.stroke(isFocus ? Color.accentColor: Color.separator)
)
.onTapGesture {
isFocus = true
}
}
}

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

func makeNSView(context: Context) -> NSTextField {
let tf = NSTextField()
tf.focusRingType = .none
tf.isBordered = false
tf.isEditable = true
tf.isSelectable = true
tf.drawsBackground = false
tf.delegate = context.coordinator
tf.placeholderString = placeholder
return tf
}

func updateNSView(
_ nsView: NSTextField,
context: Context
) {
nsView.font = NSFont.preferredFont(forTextStyle: .body, options: [:])
nsView.textColor = NSColor.labelColor
nsView.stringValue = text
}

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

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

func controlTextDidBeginEditing(_ obj: Notification) {
parent.isFocus = true
}

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

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

becomeFirstResponder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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()
}
}

textField.delegate // NSTextFieldDelegate
func controlTextDidEndEditing(_ obj: Notification) {
onFocusChange(false)
}

NSTextField and NSText

https://stackoverflow.com/questions/25692122/how-to-detect-when-nstextfield-has-the-focus-or-is-its-content-selected-cocoa

When you clicked on search field, search field become first responder once, but NSText will be prepared sometime somewhere later, and the focus will be moved to the NSText.

I found out that when NSText is prepared, it is set to self.currentEditor() . The problem is that when becomeFirstResponder()’s call, self.currentEditor() hasn’t set yet. So becomeFirstResponder() is not the method to detect it’s focus.

On the other hand, when focus is moved to NSText, text field’s resignFirstResponder() is called, and you know what? self.currentEditor() has set. So, this is the moment to tell it’s delegate that that text field got focused

Use NSTextView

Any time you want to customize NSTextField, use NSTextView instead

1
2
3
4
5
6
7
8
// NSTextViewDelegate
func textDidBeginEditing(_ notification: Notification) {
parent.isFocus = true
}

func textDidEndEditing(_ notification: Notification) {
parent.isFocus = false
}

Updated at 2021-02-23 22:32:07

How to observe focus event of NSTextField in macOS

Issue #589

becomeFirstResponder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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()
}
}

textField.delegate // NSTextFieldDelegate
func controlTextDidEndEditing(_ obj: Notification) {
onFocusChange(false)
}

NSTextField and NSText

https://stackoverflow.com/questions/25692122/how-to-detect-when-nstextfield-has-the-focus-or-is-its-content-selected-cocoa

When you clicked on search field, search field become first responder once, but NSText will be prepared sometime somewhere later, and the focus will be moved to the NSText.

I found out that when NSText is prepared, it is set to self.currentEditor() . The problem is that when becomeFirstResponder()’s call, self.currentEditor() hasn’t set yet. So becomeFirstResponder() is not the method to detect it’s focus.

On the other hand, when focus is moved to NSText, text field’s resignFirstResponder() is called, and you know what? self.currentEditor() has set. So, this is the moment to tell it’s delegate that that text field got focused

Use NSTextView

Any time you want to customize NSTextField, use NSTextView instead

1
2
3
4
5
6
7
8
// NSTextViewDelegate
func textDidBeginEditing(_ notification: Notification) {
parent.isFocus = true
}

func textDidEndEditing(_ notification: Notification) {
parent.isFocus = false
}

How to change caret color of NSTextField in macOS

Issue #588

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class FocusAwareTextField: NSTextField {
var onFocus: () -> Void = {}
var onUnfocus: () -> Void = {}

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

override func resignFirstResponder() -> Bool {
onUnfocus()
return super.resignFirstResponder()
}
}

How to make TextView in SwiftUI for macOS

Issue #587

Use NSTextVIew

From https://github.com/twostraws/ControlRoom/blob/main/ControlRoom/NSViewWrappers/TextView.swift

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

/// A wrapper around NSTextView so we can get multiline text editing in SwiftUI.
struct TextView: NSViewRepresentable {
@Binding private var text: String
private let isEditable: Bool

init(text: Binding<String>, isEditable: Bool = true) {
_text = text
self.isEditable = isEditable
}

init(text: String) {
self.init(text: Binding<String>.constant(text), isEditable: false)
}

func makeNSView(context: Context) -> NSScrollView {
let text = NSTextView()
text.backgroundColor = isEditable ? .textBackgroundColor : .clear
text.delegate = context.coordinator
text.isRichText = false
text.font = NSFont.monospacedSystemFont(ofSize: NSFont.systemFontSize, weight: .regular)
text.autoresizingMask = [.width]
text.translatesAutoresizingMaskIntoConstraints = true
text.isVerticallyResizable = true
text.isHorizontallyResizable = false
text.isEditable = isEditable

let scroll = NSScrollView()
scroll.hasVerticalScroller = true
scroll.documentView = text
scroll.drawsBackground = false

return scroll
}

func updateNSView(_ view: NSScrollView, context: Context) {
let text = view.documentView as? NSTextView
text?.string = self.text

guard context.coordinator.selectedRanges.count > 0 else {
return
}

text?.selectedRanges = context.coordinator.selectedRanges
}

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

class Coordinator: NSObject, NSTextViewDelegate {
var parent: TextView
var selectedRanges = [NSValue]()

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

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

Use xib

Create a xib called ScrollableTextView, and drag just Scrollable text view as top object

Screenshot 2020-01-29 at 06 49 55

Connect just the textView property

1
2
3
4
5
import AppKit

class ScrollableTextView: NSScrollView {
@IBOutlet var textView: NSTextView!
}

Conform to NSViewRepresentable

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

struct TextView: NSViewRepresentable {
@Binding var text: String

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

func makeNSView(context: Context) -> ScrollableTextView {
var views: NSArray?
Bundle.main.loadNibNamed("ScrollableTextView", owner: nil, topLevelObjects: &views)
let scrollableTextView = views!.compactMap({ $0 as? ScrollableTextView }).first!
scrollableTextView.textView.delegate = context.coordinator
return scrollableTextView
}

func updateNSView(_ nsView: ScrollableTextView, context: Context) {
guard nsView.textView.string != text else { return }
nsView.textView.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
}
}
}

There seems to be a bug that if we have open and close curly braces, any character typed into NSTextView will move the cursor to the end. This is easily fixed with a check in updateNSView

Updated at 2021-02-24 21:50:17

How to use custom font in SwiftUI

Issue #586

In macOS

Add fonts to target. In Info.plist, just need to specify font locations, most of the time they are at Resources folder

ATSApplicationFontsPath (String - macOS) identifies the location of a font file or directory of fonts in the bundle’s Resources directory. If present, macOS activates the fonts at the specified path for use by the bundled app. The fonts are activated only for the bundled app and not for the system as a whole. The path itself should be specified as a relative directory of the bundle’s Resources directory. For example, if a directory of fonts was at the path /Applications/MyApp.app/Contents/Resources/Stuff/MyFonts/, you should specify the string Stuff/MyFonts/ for the value of this key.

1
2
<key>ATSApplicationFontsPath</key>
<string>.</string>

Define font weight as enums, and base on ContentSizeCategory

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
enum OpenSans: String {
case bold = "OpenSans-Bold"
case semiBold = "OpenSans-SemiBold"
case extraBold = "OpenSans-ExtraBold"
case light = "OpenSans-Light"
case regular = "OpenSans-Regular"
}

enum FiraCode: String {
case bold = "FiraCode-Bold"
case medium = "FiraCode-Medium"
case light = "FiraCode-Light"
case regular = "FiraCode-Regular"
case retina = "FiraCode-Retina"
}

extension ContentSizeCategory {
var size: CGFloat {
switch self {
case .small:
return 14
case .medium:
return 16
case .large:
return 20
default:
return 14
}
}
}

extension View {
func customFont(_ font: OpenSans, category: ContentSizeCategory) -> some View {
return self.customFont(font.rawValue, category: category)
}

func customFont(_ name: String, category: ContentSizeCategory) -> some View {
return self.font(.custom(name, size: category.size))
}
}

Then we can specify font weight, thanks to function overloading

1
2
Text("Welcome")
.customFont(OpenSans.bold, category: .large)

In iOS

Observe @Environment(\.sizeCategory) var sizeCategory in ViewModifier

Read more

How to test drag and drop in UITests

Issue #583

In UITests, we can use press from XCUIElement to test drag and drop

1
2
3
4
5
let fromCat = app.buttons["cat1"].firstMatch
let toCat = app.buttons["cat2"]
let fromCoordinate = fromCat.coordinate(withNormalizedOffset: CGVector(dx: 0, dy: 0))
let toCoordinate = toCat.coordinate(withNormalizedOffset: CGVector(dx: 0, dy: -0.5))
fromCoordinate.press(forDuration: 1, thenDragTo: toCoordinate)

and then take screenshot

1
2
3
4
5
let screenshot = XCUIScreen.main.screenshot()
let attachment = XCTAttachment(screenshot: screenshot)
attachment.lifetime = .keepAlways
attachment.name = name
add(attachment)

Screenshot capturing happens after the action, so it may be too late. One way is to inject launch arguments, like app.launchArguments.append("--dragdrop") to alter some code in the app.

We can also swizzle gesture recognizer to alter behavior

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
extension UILongPressGestureRecognizer {
@objc var uiTests_state: UIGestureRecognizer.State {
let state = self.uiTests_state
if state == .ended {
return .changed
} else {
return state
}
}
}

let originalSelector = #selector(getter: UILongPressGestureRecognizer.state)
let swizzledSelector = #selector(getter: UILongPressGestureRecognizer.uiTests_state)

let originalMethod = class_getInstanceMethod(UILongPressGestureRecognizer.self, originalSelector)!
let swizzledMethod = class_getInstanceMethod(UILongPressGestureRecognizer.self, swizzledSelector)!

let didAddMethod = class_addMethod(UILongPressGestureRecognizer.self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))

if didAddMethod {
class_replaceMethod(UILongPressGestureRecognizer.self, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
} else {
method_exchangeImplementations(originalMethod, swizzledMethod)
}

How to set corner radius in iOS

Issue #582

Use View Debugging

Run on device, Xcode -> Debug -> View debugging -> Rendering -> Color blended layer
On Simulator -> Debug -> Color Blended Layer

Corner radius

Okay. Talked to a Core Animation engineer again:

  • cornerRadius was deliberately improved in Metal so it could be used everywhere.
  • Using a bitmap is WAY heavier in terms of memory and performance.
  • CALayer maskLayer is still heavy.

https://developer.apple.com/documentation/quartzcore/calayer/1410818-cornerradius

Setting the radius to a value greater than 0.0 causes the layer to begin drawing rounded corners on its background. By default, the corner radius does not apply to the image in the layer’s contents property; it applies only to the background color and border of the layer. However, setting the masksToBounds property to true causes the content to be clipped to the rounded corners.

https://developer.apple.com/documentation/quartzcore/calayer/1410896-maskstobounds

When the value of this property is true, Core Animation creates an implicit clipping mask that matches the bounds of the layer and includes any corner radius effects. If a value for the mask property is also specified, the two masks are multiplied to get the final mask value.

Mask layer

layer.cornerRadius, with or without layer.maskedCorners causes blending
Use mask layer instead of layer.cornerRadius to avoid blending, but mask causes offscreen rendering

1
2
3
4
5
6
7
8
let mask = CAShapeLayer()
let path = UIBezierPath(
roundedRect: bounds,
byRoundingCorners: [.topLeft, .topRight, .bottomLeft, .bottomRight],
cornerRadii: CGSize(width: 20, height: 20)
)
mask.path = path.cgPath
layer.mask = mask

Offscreen rendering

Instruments’ Core Animation Tool has an option called Color Offscreen-Rendered Yellow that will color regions yellow that have been rendered with an offscreen buffer (this option is also available in the Simulator’s Debug menu). Be sure to also check Color Hits Green and Misses Red. Green is for whenever an offscreen buffer is reused, while red is for when it had to be re-created.

Offscreen drawing on the other hand refers to the process of generating bitmap graphics in the background using the CPU before handing them off to the GPU for onscreen rendering. In iOS, offscreen drawing occurs automatically in any of the following cases:

Core Graphics (any class prefixed with CG)
The drawRect() method, even with an empty implementation.
CALayers with a shouldRasterize property set to YES.
CALayers using masks (setMasksToBounds) and dynamic shadows (setShadow
).
Any text displayed on screen, including Core Text.
Group opacity (UIViewGroupOpacity).

Instruments

Read more

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@abstract Sets the corner rounding method to use on the ASDisplayNode.
There are three types of corner rounding provided by Texture: CALayer, Precomposited, and Clipping.

- ASCornerRoundingTypeDefaultSlowCALayer: uses CALayer's inefficient .cornerRadius property. Use
this type of corner in situations in which there is both movement through and movement underneath
the corner (very rare). This uses only .cornerRadius.

- ASCornerRoundingTypePrecomposited: corners are drawn using bezier paths to clip the content in a
CGContext / UIGraphicsContext. This requires .backgroundColor and .cornerRadius to be set. Use opaque
background colors when possible for optimal efficiency, but transparent colors are supported and much
more efficient than CALayer. The only limitation of this approach is that it cannot clip children, and
thus works best for ASImageNodes or containers showing a background around their children.

- ASCornerRoundingTypeClipping: overlays 4 separate opaque corners on top of the content that needs
corner rounding. Requires .backgroundColor and .cornerRadius to be set. Use clip corners in situations
in which is movement through the corner, with an opaque background (no movement underneath the corner).
Clipped corners are ideal for animating / resizing views, and still outperform CALayer.

Generally, on iOS, pixel effects and Quartz / Core Graphics drawing are not hardware accelerated, and most other things are.
The following things are not hardware accelerated, which means that they need to be done in software (offscreen):
Anything done in a drawRect. If your view has a drawRect, even an empty one, the drawing is not done in hardware, and there is a performance penalty.
Any layer with the shouldRasterize property set to YES.
Any layer with a mask or drop shadow.
Text (any kind, including UILabels, CATextLayers, Core Text, etc).
Any drawing you do yourself (either onscreen or offscreen) using a CGContext.

For example, writing your own draw method with Core Graphics means your rendering will technically be done in software (offscreen) as opposed to being hardware accelerated like it is when you use a normal CALayer. This is why manually rendering a UIImage with a CGContext is slower than just assigning the image to a UIImageView.

if layer’s contents is nil or this contents has a transparent background, you just need to set cornerRadius. For UILabel, UITextView and UIButton, you can just set layer’s backgroundColor and cornerRadius to get a rounded corner. Note: UILabel’s backgroundColor is not its layer’s backgroundColor.

How to work with SceneDelegate in iOS 12

Issue #580

Events

open url

Implement scene(_:openURLContexts:) in your scene delegate.

If the URL launches your app, you will get scene(_:willConnectTo:options:) instead and it’s in the options.

life cycle

Here’s how it works: If you have an “Application Scene Manifest” in your Info.plist and your app delegate has a configurationForConnectingSceneSession method, the UIApplication won’t send background and foreground lifecycle messages to your app delegate. That means the code in these methods won’t run:

applicationDidBecomeActive
applicationWillResignActive
applicationDidEnterBackground
applicationWillEnterForeground
The app delegate will still receive the willFinishLaunchingWithOptions: and didFinishLaunchingWithOptions: method calls so any code in those methods will work as before.

UIApplication notifications

Notifications still trigger in iOS 13 if adopting SceneDelegate

1
2
UIApplication.didBecomeActiveNotification
UIApplication.willResignActiveNotification

https://developer.apple.com/documentation/uikit/app_and_environment/scenes/preparing_your_ui_to_run_in_the_foreground

Use foreground transitions to prepare your app’s UI to appear onscreen. An app’s transition to the foreground is usually in response to a user action. For example, when the user taps the app’s icon, the system launches the app and brings it to the foreground. Use a foreground transition to update your app’s UI, acquire resources, and start the services you need to handle user requests.

All state transitions result in UIKit sending notifications to the appropriate delegate object:

In iOS 13 and later—A UISceneDelegate object.
In iOS 12 and earlier—The UIApplicationDelegate object.

You can support both types of delegate objects, but UIKit always uses scene delegate objects when they are available. UIKit notifies only the scene delegate associated with the specific scene that is entering the foreground. For information about how to configure scene support, see Specifying the Scenes Your App Supports.

keyWindow

Show most recent activeUIWindow

1
UIApplication.shared.keyWindow

This property holds the UIWindow object in the windows array that is most recently sent the makeKeyAndVisible() message.

AppDelegate vs SceneDelegate

Get sceneDelegate from AppDelegate

1
UIApplication.shared.openSessions.first?.scene?.delegate

order

1
2
3
4
SceneDelegate.sceneDidBecomeActive
UIApplication.didBecomeActiveNotification
SceneDelegate.sceneWillResignActive
UIApplication.willResignActiveNotification

Read more

How to handle radio group for NSButton

Issue #579

Use same action, or we can roll our own implementation

An NSButton configured as a radio button (with the -buttonType set to NSRadioButton), will now operate in a radio button group for applications linked on 10.8 and later. To have the button work in a radio group, use the same -action for each NSButton instance, and have the same superview for each button. When these conditions are met, checking one button (by changing the -state to 1), will uncheck all other buttons (by setting their -state to 0).

1
2
3
4
5
6
7
import Omnia

@IBAction func onModeButtonTouch(_ sender: NSRadioButton) {
for button in [mode1Button, mode2Button] {
button?.isOn = button === sender
}
}

How to specify locale in Swift

Issue #578

Locale

Read Language and Locale IDs

1
2
zh-Hans_HK
[language designator]-[script designator]_[region designator]

Language IDs

A language ID identifies a language used in many regions, a dialect used in a specific region, or a script used in multiple regions. To specify a language used in many regions, use a language designator by itself. To specify a specific dialect, use a hyphen to combine a language designator with a region designator. To specify a script, combine a language designator with a script designator. For example, to specify common English, use the en language designator as the language ID. To specify the English language as it is used in the United Kingdom, use en-GB as the language ID.

Locale IDs

A locale ID identifies a specific region and its cultural conventions—such as the formatting of dates, times, and numbers. To specify a locale, use an underscore character to combine a language ID with a region designator, as shown in Table B-5. For example, the locale ID for English-language speakers in the United Kingdom is en_GB, while the locale for English-speaking residents of the United States is en_US.

Example

1
2
3
4
5
6
7
8
9
10
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .short
formatter.locale = Locale.current

10. jan. 2018, 16:50 // nb_NO
10. jan. 2018, 16:50 // nb_US iOS 12
10. jan. 2018, 4:50 pm // nb_US iOS 13
10 Jan 2018 at 16:50 // en_NO
Jan 10, 2018 at 4:50 PM // en_US

Date format

Use DateFormatter Style

Example short

Specifies a short style, typically numeric only, such as “11/23/37” or “3:30 PM”.

Example medium

Specifies a medium style, typically with abbreviated text, such as “Nov 23, 1937” or “3:30:32 PM”.

From template

If you need to define a format that cannot be achieved using the predefined styles, you can use the setLocalizedDateFormatFromTemplate(_:) to specify a localized date format from a template.

1
2
3
4
5
6
7
8
9
10
11
12
let dateFormatter = DateFormatter()
let date = Date(timeIntervalSinceReferenceDate: 410220000)

// US English Locale (en_US)
dateFormatter.locale = Locale(identifier: "en_US")
dateFormatter.setLocalizedDateFormatFromTemplate("MMMMd") // set template after setting locale
print(dateFormatter.string(from: date)) // December 31

// British English Locale (en_GB)
dateFormatter.locale = Locale(identifier: "en_GB")
dateFormatter.setLocalizedDateFormatFromTemplate("MMMMd") // // set template after setting locale
print(dateFormatter.string(from: date)) // 31 December

Read more

How to workaround URLSession issue in watchOS 6.1.1

Issue #577

https://stackoverflow.com/questions/59724731/class-avassetdownloadtask-is-implemented-in-both-cfnetwork-and-avfoundation

1
2
3
objc[45250]: Class AVAssetDownloadTask is implemented in both /Applications/Xcode.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/CFNetwork.framework/CFNetwork (0x4ddd0ec) and /Applications/Xcode.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/AVFoundation.framework/AVFoundation (0x16aea494). One of the two will be used. Which one is undefined.

objc[45250]: Class AVAssetDownloadURLSession is implemented in both /Applications/Xcode.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/CFNetwork.framework/CFNetwork (0x4dddd44) and /Applications/Xcode.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/AVFoundation.framework/AVFoundation (0x16aea4bc). One of the two will be used. Which one is undefined.

Then URLSession stops working.

1
2020-01-13 22:50:12.430920+0100 MyAppWatch WatchKit Extension[45250:2099229] Task <3CECDE81-59B9-4EDE-A4ED-1BA173646037>.<1> finished with error [-999] Error Domain=NSURLErrorDomain Code=-999 "cancelled" UserInfo={NSErrorFailingURLKey=https://myapp.com/def.json, NSErrorFailingURLStringKey=https://myapp.com/def.json, NSLocalizedDescription=cancelled}

The workaround is to remove Combine based API, and use completion block.

Instead of dataTaskPublisher which hangs indefinitely, no sink is reported

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
URLSession.shared
.dataTaskPublisher(for: url)
.map({ $0.data })
.decode(type: T.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
.receive(on: RunLoop.main)
.sink(receiveCompletion: { completionStatus in
switch completionStatus {
case .finished:
break
case .failure(let error):
completion(.failure(error))
}
}, receiveValue: { value in
completion(.success(value))
})

just use normal

1
2
3
4
5
6
7
8
9
let task = URLSession.shared.dataTask(with: url, completionHandler: { data, response, error in
if let data = data, let model = try? JSONDecoder().decode(T.self, from: data) {
completion(.success(model))
} else {
completion(.failure(error ?? ServiceError.noInternet))
}
})

task.resume()

How to generate XCTest test methods

Issue #576

Code

See Spek

Override testInvocations to specify test methods

https://developer.apple.com/documentation/xctest/xctestcase/1496271-testinvocations

Returns an array of invocations representing each test method in the test case.

Because testInvocations is unavailable in Swift, we need to use ObjC

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
#import "include/SpekHelperTestCase.h"

@implementation SpekHelperTestCase

- (instancetype)init {
self = [super initWithInvocation: nil];
return self;
}

+ (NSArray<NSInvocation *> *)testInvocations {
NSArray<NSString *> *selectorStrings = [self spekGenerateTestMethodNames];
NSMutableArray<NSInvocation *> *invocations = [NSMutableArray arrayWithCapacity:selectorStrings.count];

for (NSString *selectorString in selectorStrings) {
SEL selector = NSSelectorFromString(selectorString);
NSMethodSignature *signature = [self instanceMethodSignatureForSelector:selector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.selector = selector;

[invocations addObject:invocation];
}

return invocations;
}

+ (NSArray<NSString *> *)spekGenerateTestMethodNames {
return @[];
}

@end

Generate test methods

Calculate based on Describe and It, and use Objc runtime class_addMethod to add instance methods

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
open class SpekTestCase: SpekHelperTestCase {
open class func makeDescribe() -> Describe {
return Describe("empty")
}

#if canImport(SpekHelper)

override public class func spekGenerateTestMethodNames() -> [String] {
let describe = Self.makeDescribe()

var names: [String] = []
generate(describe: describe, names: &names)
return names
}

private static func addInstanceMethod(name: String, closure: @escaping () -> Void) -> String {
let block: @convention(block) (SpekTestCase) -> Void = { spekTestCase in
let _ = spekTestCase
closure()
}

let implementation = imp_implementationWithBlock(block as Any)
let selector = NSSelectorFromString(name)
class_addMethod(self, selector, implementation, "v@:")

return name
}
}

Read more

How to use ObjC in Swift Package Manager

Issue #575

Create Objc target

Check runtime

Check for example _runtime(_ObjC) or os(macOS if you plan to use platform specific feature

For example, in test we use XCTest which is run via Xcode and is a macOS framework, so we need to check for os(macOS)

Note that in Objc framework, the header files must be in include folder

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
targets: {
var targets: [Target] = [
.testTarget(
name: "QuickTests",
dependencies: [ "Quick", "Nimble" ],
exclude: [
"QuickAfterSuiteTests/AfterSuiteTests+ObjC.m",
"QuickFocusedTests/FocusedTests+ObjC.m",
"QuickTests/FunctionalTests/ObjC",
"QuickTests/Helpers/QCKSpecRunner.h",
"QuickTests/Helpers/QCKSpecRunner.m",
"QuickTests/Helpers/QuickTestsBridgingHeader.h",
"QuickTests/QuickConfigurationTests.m",
]
),
]
#if os(macOS)
targets.append(contentsOf: [
.target(name: "QuickSpecBase", dependencies: []),
.target(name: "Quick", dependencies: [ "QuickSpecBase" ]),
])
#else
targets.append(contentsOf: [
.target(name: "Quick", dependencies: []),
])
#endif
return targets
}(),

How to expression cast type in lldb in Swit

Issue #574

1
2
3
expr -l Swift -- import UIKit
expr -l Swift -- let $collectionView = unsafeBitCast(0x7fddd8180000, to: UICollectionView.self)
expr -l Swift -- $collectionView.safeAreaInsets

How to disable implicit decoration view animation in UICollectionView

Issue #569

From documentation https://developer.apple.com/documentation/uikit/uicollectionviewlayout/1617726-initiallayoutattributesforappear

This method is called after the prepare(forCollectionViewUpdates:) method and before the finalizeCollectionViewUpdates() method for any decoration views that are about to be inserted. Your implementation should return the layout information that describes the initial position and state of the view. The collection view uses this information as the starting point for any animations. (The end point of the animation is the view’s new location in the collection view.) If you return nil, the layout object uses the item’s final attributes for both the start and end points of the animation.

The default implementation of this method returns nil.

Although the doc says “The default implementation of this method returns nil”, calling super.initialLayoutAttributesForAppearingDecorationElement gives somehow implicit animation. The workaround is to explicitly return nil

1
2
3
4
5
6
7
func initialLayoutAttributesForAppearingDecorationElement(ofKind elementKind: String, at decorationIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return nil
}

func finalLayoutAttributesForDisappearingDecorationElement(ofKind elementKind: String, at decorationIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return nil
}

Decoration seems to be removed when all items are removed. Workaround is to check and only add decoration when there is preferred data or cell