How to deal with weak in closure in Swift

Issue #326

Traditionally, from Swift 4.2 we need guard let self

1
2
3
4
5
6
7
8
9
addButton.didTouch = { [weak self] in
guard
let self = self,
let product = self.purchasedProduct()
else {
return

self.delegate?.productViewController(self, didAdd: product)
}

This is cumbersome, we can invent a higher order function to zip and unwrap the optionals

1
2
3
4
5
6
7
8
9
10
11
func with<A, B>(_ op1: A?, _ op2: B?, _ closure: (A, B) -> Void) {
if let value1 = op1, let value2 = op2 {
closure(value1, value2)
}
}

addButton.didTouch = { [weak self] in
with(self, self?.purchasedProduct()) {
$0.delegate?.productViewController($0, didAdd: $1)
}
}

How to make material UITextField with floating label in iOS

Issue #325

  • Use UILabel as placeholder and move it
  • When label is moved up, scale it down 80%. It means it has 10% padding on the left and right when shrinked, so offsetX for translation is 10%
  • Translation transform should happen before scale
  • Ideally we can animate font and color change using CATextLayer, but with UILabel we can use UIView.transition
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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
final class MaterialInputView: UIView {
lazy var label: UILabel = {
return UILabel()
}()

lazy var textField: UITextField = {
let textField = UITextField()
textField.tintColor = R.color.primary
textField.textColor = R.color.lightText
textField.font = R.customFont.medium(16)
textField.autocapitalizationType = .none
textField.autocorrectionType = .no

return textField
}()

lazy var line: UIView = {
let line = UIView()
line.backgroundColor = R.color.primary
return line
}()

// Whether label should be moved to top
private var isUp: Bool = false {
didSet {
styleLabel(isUp: isUp)
moveLabel(isUp: isUp)
}
}

override init(frame: CGRect) {
super.init(frame: frame)

setup()
}

required init?(coder aDecoder: NSCoder) {
fatalError()
}

private func setup() {
addSubviews([textField, label, line])
textField.delegate = self

NSLayoutConstraint.on([
textField.leftAnchor.constraint(equalTo: leftAnchor, constant: 16),
textField.rightAnchor.constraint(equalTo: rightAnchor, constant: -16),
textField.topAnchor.constraint(equalTo: topAnchor, constant: 16),

label.leftAnchor.constraint(equalTo: textField.leftAnchor),
label.centerYAnchor.constraint(equalTo: textField.centerYAnchor),

line.leftAnchor.constraint(equalTo: textField.leftAnchor),
line.rightAnchor.constraint(equalTo: textField.rightAnchor),
line.topAnchor.constraint(equalTo: textField.bottomAnchor, constant: 8),
line.heightAnchor.constraint(equalToConstant: 2)
])

styleLabel(isUp: false)
}

private func styleLabel(isUp: Bool) {
UIView.transition(
with: label,
duration: 0.15,
options: .curveEaseInOut,
animations: {
if isUp {
self.label.font = R.customFont.regular(12)
self.label.textColor = R.color.primary
} else {
self.label.font = R.customFont.medium(16)
self.label.textColor = R.color.grayText
}
},
completion: nil
)
}

private func moveLabel(isUp: Bool) {
UIView.animate(
withDuration: 0.15,
delay: 0,
options: .curveEaseInOut,
animations: {
if isUp {
let offsetX = self.label.frame.width * 0.1
let translation = CGAffineTransform(translationX: -offsetX, y: -24)
let scale = CGAffineTransform(scaleX: 0.8, y: 0.8)
self.label.transform = translation.concatenating(scale)
} else {
self.label.transform = .identity
}
},
completion: nil
)
}
}

extension MaterialInputView: UITextFieldDelegate {
func textFieldDidBeginEditing(_ textField: UITextField) {
if !isUp {
isUp = true
}
}

func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
guard let text = textField.text else {
return false
}

if isUp && text.isEmpty {
isUp = false
}
return true
}
}

How to embed UIStackView inside UIScrollView in iOS

Issue #324

1
2
3
4
5
6
7
8
9
10
11
view.addSubview(scrollView)
scrollView.addSubview(stackView)

NSLayoutConstraint.on([
scrollView.pinEdges(view: view),
stackView.pinEdges(view: scrollView)
])

NSLayoutConstraint.on([
stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, multiplier: 1.0)
])

Updated at 2020-09-07 14:28:15

How to animate NSCollectionView changes

Issue #323

Use proxy animator()

1
2
3
4
5
let indexPath = IndexPath(item: index, section: 0)
collectionView.animator().deleteItems(at: Set(arrayLiteral: indexPath))

let indexPath = IndexPath(item: 0, section: 0)
collectionView.animator().insertItems(at: Set(arrayLiteral: indexPath))

How to handle right click in AppKit

Issue #322

1
2
3
4
5
lazy var gr = NSClickGestureRecognizer(target: self, action: #selector(onPress(_:)))

gr.buttonMask = 0x2
gr.numberOfClicksRequired = 1
view.addGestureRecognizer(gr)

How to show context menu in NSCollectionView

Issue #321

Detect locationInWindow in NSEvent

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
class ClickedCollectionView: NSCollectionView {
var clickedIndex: Int?

override func menu(for event: NSEvent) -> NSMenu? {
clickedIndex = nil

let point = convert(event.locationInWindow, from: nil)
for index in 0..<numberOfItems(inSection: 0) {
let frame = frameForItem(at: index)
if NSMouseInRect(point, frame, isFlipped) {
clickedIndex = index
break
}
}

return super.menu(for: event)
}
}

let menu = NSMenu()
menu.addItem(NSMenuItem(title: "Delete", action: #selector(didSelectDelete(_:)), keyEquivalent: ""))
collectionView.menu = menu

@objc func didSelectDelete(_ item: NSMenuItem) {
guard
let index = collectionView.clickedIndex,
index < notes.count
else {
return
}

let indexPath = IndexPath(item: index, section: 0)
notes.remove(at: index)
collectionView.deleteItems(at: Set(arrayLiteral: indexPath))
}

For NSCollectionView with more than 1 sections

1
let frame = layoutAttributesForItem(at: IndexPath(item: index, section: 0))?.frame ?? .zero

Use Omnia

Omnia supports clicked indexPath for multi section NSCollectionView

1
2
3
collectionViewHandler.addMenuItem(title: "Add to Favorite", action: { item in
print(item)
})

How to customize NSTextView in AppKit

Issue #320

Scrollable

textview

Embed image or NSTextAttachmentCellProtocol

  • Select TextView
  • Select Rich Text and Graphics
  • Select Size Inspector -> Resizable and tick both Horizontally and Vertically

Customize

1
2
3
4
5
6
7
8
9
10
11
12
13
14
scrollView.drawsBackground = false
textView.drawsBackground = false
textView.string = "What's on your mind?"
textView.delegate = self
textView.selectedTextAttributes = [
NSAttributedString.Key.backgroundColor: NSColor(hex: "414858"),
NSAttributedString.Key.foregroundColor: NSColor(hex: "ACB2BE")
]

extension MainView: NSTextViewDelegate {
func textViewDidChangeSelection(_ notification: Notification) {
// Change text color again after image dragging
}
}

How to use custom font in AppKit

Issue #319

  • Add fonts to targets
  • Declare in Info.plist

https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/GeneralPurposeKeys.html#//apple_ref/doc/uid/TP40009253-SW8

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>
  • Reference by name
1
NSFont(name: "FiraCode-Bold", size: 14)

How to make custom controller for View in iOS

Issue #318

I do UI in code, and usually separate between View and ViewController.

1
2
3
4
5
6
7
class ProfileView: UIView {}

class ProfileViewController: UIViewController {
override func loadView() {
self.view = ProfileView()
}
}

But in places where using UIViewController and manage their view controller containment hierarchy is not desired, then we can roll out a normal object to act as the controller.

1
2
3
4
5
6
7
8
9
10
11
class ProfileController {
let profileView: ProfileView

init(profileView: ProfileView) {
self.profileView = profileView
}

func update(profile: Profile) {
profileView.nameLabel.text = profile.name
}
}

If the name Controller sounds confusing with UIViewController, I usually use Handler, which contains other Handler to handle logic for view

How to avoid crash when closing NSWindow for agent macOS app

Issue #312

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ClosableWindow: NSWindow {
override func close() {
self.orderOut(NSApp)
}
}

let window = ClosableWindow(
contentRect: rect,
styleMask: [.titled, .closable],
backing: .buffered,
defer: false
}

window.makeKeyAndOrderFront(NSApp)

The reason is that window is released upon closed if it is not owned by NSWindowController, or we can use releasedWhenClosed

The value of this property is YES if the window is automatically released after being closed; NO if it’s simply removed from the screen.

The default for NSWindow is YES; the default for NSPanel is NO. Release when closed, however, is ignored for windows owned by window controllers. Another strategy for releasing an NSWindow object is to have its delegate autorelease it on receiving a windowShouldClose: message.


Updated at 2020-12-14 05:18:30

How to get cell at center during scroll in UICollectionView

Issue #311

See Omnia https://github.com/onmyway133/Omnia/blob/master/Sources/iOS/UICollectionView.swift#L30

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
extension HorizontalUsersViewController: UIScrollViewDelegate {
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let point = view.convert(collectionView.center, to: collectionView)

guard
let indexPath = collectionView.indexPathForItem(at: point),
indexPath.item < users.count
else {
return
}

let user = users[indexPath.item]
delegate?.didScrollTo(user)
}
}

How to show location in Apple Maps and Google Maps app in iOS

Issue #309

Apple Maps

1
2
3
4
let placemark = MKPlacemark(coordinate: coordinate, addressDictionary: nil)
let mapItem = MKMapItem(placemark: placemark)
mapItem.name = shop.name
mapItem.openInMaps(launchOptions: [:])

Google Maps

Since iOS 9, we need to declare LSApplicationQueriesSchemes

1
2
3
4
<key>LSApplicationQueriesSchemes</key>
<array>
<string>comgooglemaps</string>
</array>
1
2
3
4
5
6
7
8
9
10

var string = "comgooglemaps://"
string += "?q=Food"
string += "&center=\(coordinate.latitude),\(coordinate.longitude)"
string += "&zoom=15"
let googleUrl = URL(string: string)!

if UIApplication.shared.canOpenURL(URL(string: "comgooglemaps://")!) {
UIApplication.shared.open(googleUrl)
}

How to make convenient touch handler for UIButton in iOS

Issue #308

If you don’t want to use https://github.com/onmyway133/EasyClosure yet, it’s easy to roll out a closure based UIButton. The cool thing about closure is it captures variables

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
final class ClosureButton: UIButton {
var didTouch: (() -> Void)?

override init(frame: CGRect) {
super.init(frame: frame)

addTarget(self, action: #selector(buttonTouched(_:)), for: .touchUpInside)
}

required init?(coder aDecoder: NSCoder) {
fatalError()
}

@objc private func buttonTouched(_: UIButton) {
didTouch?()
}
}

Then in cellForItem

1
2
3
4
5
6
7
8
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell: UserCell = collectionView.dequeue(for: indexPath)!
let user = users[indexPath.item]
cell.powerButton.didTouch = { [weak self] in
self?.openPowerView(user)
}
return cell
}

With this we can even forward touch event to another button

1
2
3
4
5
func forwardTouchEvent(button: ClosureButton) {
didTouch = { [weak button] in
button?.didTouch?()
}
}

Another benefit is that we can apply debouncing to avoid successive tap on button

1
2
3
4
5
6
let debouncer = Debouncer(timeInterval: 0.2)
@objc private func buttonTouched(_: UIButton) {
debouncer.run { [weak self] in
self?.didTouch?()
}
}

How to format distance in iOS

Issue #307

1
2
3
4
5
6
import MapKit

let formatter = MKDistanceFormatter()
formatter.unitStyle = .abbreviated
formatter.units = .metric
distanceLabel.text = formatter.string(fromDistance: distance) // 700m, 1.7km

How to mock grpc model in Swift

Issue #306

1
2
3
4
5
6
7
8
9
let json: [String: Any] = [
"id": "123",
"name": "Thor",
"isInMarvel": true
]

let data = try JSONSerialization.data(withJSONObject: json, options: [])
let string = String(data: data, encoding: .utf8)!
return try Hero(jsonString: string)

If we use withValue from How to simplify struct mutating in Swift then we can mock easily

1
2
3
4
5
6
7
8
9
extension Hero {
static func mock() -> Hero {
return withValue(Hero()) {
$0.id = "123"
$0.name = "Thor"
$0.isInMarvel = true
}
}
}

Favorite WWDC 2019 sessions

Issue #305

w1

This year I’m lucky enough to get the ticket to WWDC and I couldn’t be more satisfied. 5 conference days full of awesomeness, talks, labs and networking, all make WWDC special and memorial conference for every attendee.

As predicted by many, Marzipan (now officially called Project Catalyst) a technology that could allow iOS apps to be ported to macOS, would be the main topic for this year. But WWDC went more spectacular than that, with dark mode on iOS, independent watchOS apps, and SwiftUI being the star of the show. With over 150 sessions and extra videos, it can be a bit overwhelming to catch up, so I sum up 10 essential sessions to get started. It’s good to catch up with the latest technology, but be aware that frameworks and APIs come and get deprecated very often. It’s better to understand why they are introduced, how to learn the skills and mindset so we can apply them in our apps to delight user experience.

Firstly, a little tip to get the most of WWDC videos. Although you can watch on Apple developer website, there’s WWDC for macOS app that allows much more comfortable watching experience. There we can tweak playing speed, picture in picture view mode, favorite and download videos for offline watching.

Secondly, for those of you who want to search some texts in the talks, there is ASCIIwwdc that provides full transcripts of all the talks.

1. Platform State of the Union

If you only have time for 1 video, this is it. Right after the Keynote, Platform State of the Union is like keynote for developers as it highlights important development changes.

  • macOS 10.15, iOS 13, watchOS 6 and tvOS 13: As usual we get version bumps on all major platforms, which brings lots of new features and enhancement. macOS 10.15 is caleld Catalina and there’s a whole new platform for iPad called iPadOS.
  • Security and Privacy: Adding to security enhancement from last year, this year shows how Apple really commits into this. There are one-time location permission, signing with Apple, security protocol for HomeKit, new crypto framework which marks MD5 as insecure. Also, apps that target kids can’t display ad or include analytics.
  • tvOS 13 gets multiple user support
  • watchOS 6 makes way for independent watch apps, which does not require accompanying iOS apps. There’s also dedicated watch appstore.
  • iOS 13 now can live in the dark, but dropping support for iPhone 5S, 6 and below. Also, there is ability to toggle language setting per app only.
  • iPadOS is a spinoff version of iOS for now, they look the same but they are expected to take different paths. It includes mouse support and requires iPad Air 2 and newer devices.
  • macOS 10.15 introduces a replacement of bash with zsh. It also supports SideCar which allows iPad as an external display. Last but not least, there is Project Catalyst that enables iPad apps to run on the mac.
  • Xcode 11 includes Swift 5.1 that can target latest SDKs. It brings a new look and feel with tons of features like minimap, Xcode preview, official support for Swift Package Manager, source control enhancement and test plan.

2. What’s New in Swift

Although Swift is developed in the open, it’s easy to lose track of in tons of proposals and changes. Swift 5.1 brings lots of cool features that power SwiftUI and Combine, so it’s a prerequisite.

  • Module stability: This looks unimportant but this may be the most key feature of Swift 5.1. Unlike ABI stability we get in Swift 5, module stability helps resolves differences at compile time. In other words, this ensures a Swift 5 library will work with the future Swift compilers.
a1
  • A single expression can be declared without return keyword
  • Function builder, with marker @_functionBuilder which works pretty much like function with receiver in Kotlin, allows for some very neat DSL syntax.
  • Property wrapper, a counterpart of Kotlin delegated property, allows property accessors to be used in a convenient way. Shipped with Swift 5.1, we can use that with @propertyWrapper annotation.
  • Opaque return type with some keyword remedies limitation of Swift protocol with Self or associcated types requirements.
  • Among other things, there are other cool features like universal Self, static subscripts, collection diffing and matching against optional.

3. Introducing SwiftUI: Building Your First App

Welcome to the spotlight of WWDC 2019, SwiftUI. It may be the most exciting announcement since Swift was introduced in 2014. SwiftUI is not just a new framework, it’s a complete paradigm shift from imperative programming with UIKit/AppKit to a declarative world. I was amazed by how quickly React and Flutter allows fast prototyping and developing, so I’m very happy Apple finally makes this available natively on all platforms.

w2

The cool thing about SwiftUI is that it is expressive and has consistent syntax across platforms. So it is a learn once, write anywhere concept. Together with hot reloading of Xcode Preview, this ends the long debate among iOS community about whether to write UI in code or Storyboard, as the source of truth is now the concise code, but users are free to change any UI details via interactive Preview.

Not only SwiftUI handles consistent UI according to Apple design guideline, it also provides many features for free like accessibility, dark mode and other bookkeeping.

SwiftUI supports latest platform versions and no backward compatibility, so some of us have to wait 1 or 2 more years until iOS 13 is widely adopted. To learn more about SwiftUI, there are other advanced sesions like

4. Implementing Dark Mode on iOS

As much as I was excited about Dark theme in Android Q, Dark Mode in iOS is something that eases my eyes. Some apps also support Dark theme by their owns, but with iOS 13, supporting Dark mode to our apps is not a daunting task. There are more vibrancy materials, system colors that adapts automatically to dark and light modes. We can also select images for each mode in Asset Catalog easily.

w4

5. Introducing Combine and Advances in Foundation

Combine is a unified declarative framework for processing values over time. As a huge fan of Rx, Combine looks like home. It was thrilled to see this finnaly get supported official by Apple. This simplifying asyncrhonous programming a lot, also streamline other communication patterns like KVO and notification center.

w5

Combine is the force the powers reactive nature in SwiftUI with Binding and BindableObject. There’s also lots of improvements to Foundation like collection diffing, new list and relative formatters, and notably Combine syntax for URLSession, which makes networking a breeze.

To learn more about Combine, there’s Combine in Practice where we can learn more about error handling, schedule work and many operators on streams.

w6

6. Modernizing Your UI for iOS 13

Take a look at this talk to learn about new features in iOS 13 that we should be aware in our apps. Newly in iOS 13, we can take advantage of card style modal presentation that is very cumbersome to replicate ourselves. There’s also new UISearchBarTextField with advanced customizations for token and inputs. Lastly, the new UIMenu makes showing context menu trivial and make way for iPad apps to behave like native on the mac.

w8

7. Modern Swift API Design

If you’re developing iOS apps, chances are that you have already stumbled upon API Design Guidelines which contains valuable guides to structuring our Swift code.

w7

This highlights my most favorite programming advice “Clarity at the point of use”, because things we declare are written only once, but read many many times, so we should make those concise and clear. There’s also mention of preferring generic over protocol which reminds me of protocol witness technique.

This talk details how Apple engineers themselves design Swift code in their RealityKit and SwiftUI frameworks.

8. Optimizing App Launch

The launch time of your app can be decisive in user experience, it needs to be fast and do just the necessary things. With enhancements via shared and cached frameworks, apps now load faster on iOS 13. But there’s more thing we can do to improve this, thanks to the new App Launch profiler in Xcode 11, together with app launch time measurement in XCTests. The 3 words we can take away from this talk is minimize, prioritize, and optimize work at this critical launch time.

w9

9. Introducing iPad Apps for Mac

Starting with iOS 13 with Project Catalyst, there’s a new target environment check called UIKitForMac, which allows iPad apps to target the mac while using the same code base. Most of the UI after porting have the correct look and feel like a native macOS app with many features provided for free like window management. There are, kind of obviously, some frameworks that are designed specifically for phone and tablet experience, can’t be supported in macOS.

w11

There are other sesions like Taking iPad Apps for Mac to the Next Level where we can learn more about this.

10. Creating Independent Watch Apps

w13

watchOS finally gets its own Appstore and the ability to run independent watchOS apps without a companying iOS app. With the introduction of URLSession and streaming APIs for the watch, together with enhancements in push notifications, Apple sign in, debugging, this can’t be a better time to start developing for the watch.


It is stunning to see how Apple comes up with so many cool announcements this year while keeping innovation and quality high. There are more sessions to explore, head over to WWDC 2019 developer website to learn more.


Updated at 2021-01-01 23:27:16

How to test drive view in iOS

Issue #303

Instead of setting up custom framework and Playground, we can just display that specific view as root view controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
window.rootViewController = makeTestPlayground()

func makeTestPlayground() -> UIViewController {
let content = UserCell()
content.nameLabel.text = "Thor"
content.streetLabel.text = "Asgard"
content.weaponLabel.text = "Hammer"

let viewController = UIViewController()
viewController.view.backgroundColor = .white
viewController.view.addSubview(content)

NSLayoutConstraint.on([
content.centerXAnchor.constraint(equalTo: viewController.view.centerXAnchor),
content.centerYAnchor.constraint(equalTo: viewController.view.centerYAnchor),
content.widthAnchor.constraint(equalTo: viewController.view.widthAnchor, multiplier0.9),
content.heightAnchor.constraint(equalToConstant: 200)
]
return viewController
}

How to make carousel layout for UICollectionView in iOS

Issue #302

Based on AnimatedCollectionViewLayout

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
final class CarouselLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let attributes = super.layoutAttributesForElements(in: rect) else { return nil }
guard let collectionView = collectionView else { return nil }
return attributes.map({ transform(collectionView: collectionView, attribute: $0) })
}

override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}

private func transform(collectionView: UICollectionView, attribute: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
let a = attribute
let width = collectionView.frame.size.width
let itemOffset = a.center.x - collectionView.contentOffset.x
let middleOffset = (itemOffset / width) - 0.5

change(
width: collectionView.frame.size.width,
attribute: attribute,
middleOffset: middleOffset
)

return attribute
}

private func change(width: CGFloat, attribute: UICollectionViewLayoutAttributes, middleOffset: CGFloat) {
let alpha: CGFloat = 0.8
let itemSpacing: CGFloat = 0.21
let scale: CGFloat = 1.0

let scaleFactor = scale - 0.1 * abs(middleOffset)
let scaleTransform = CGAffineTransform(scaleX: scaleFactor, y: scaleFactor)

let translationX = -(width * itemSpacing * middleOffset)
let translationTransform = CGAffineTransform(translationX: translationX, y: 0)

attribute.alpha = 1.0 - abs(middleOffset) + alpha
attribute.transform = translationTransform.concatenating(scaleTransform)
}
}

How to use

1
2
3
4
5
6
let layout = CarouselLayout()

layout.scrollDirection = .horizontal
layout.sectionInset = .zero
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 0

We can inset cell content and use let scale: CGFloat = 1.0 to avoid scaling down center cell

Based on CityCollectionViewFlowLayout

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
import UIKit

class CityCollectionViewFlowLayout: UICollectionViewFlowLayout {

fileprivate var lastCollectionViewSize: CGSize = CGSize.zero

var scaleOffset: CGFloat = 200
var scaleFactor: CGFloat = 0.9
var alphaFactor: CGFloat = 0.3
var lineSpacing: CGFloat = 25.0

required init?(coder _: NSCoder) {
fatalError()
}

init(itemSize: CGSize) {
super.init()
self.itemSize = itemSize
minimumLineSpacing = lineSpacing
scrollDirection = .horizontal
}

func setItemSize(itemSize: CGSize) {
self.itemSize = itemSize
}

override func invalidateLayout(with context: UICollectionViewLayoutInvalidationContext) {
super.invalidateLayout(with: context)

guard let collectionView = self.collectionView else { return }

if collectionView.bounds.size != lastCollectionViewSize {
configureContentInset()
lastCollectionViewSize = collectionView.bounds.size
}
}

override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = self.collectionView else {
return proposedContentOffset
}

let proposedRect = CGRect(x: proposedContentOffset.x, y: 0, width: collectionView.bounds.width, height: collectionView.bounds.height)
guard let layoutAttributes = self.layoutAttributesForElements(in: proposedRect) else {
return proposedContentOffset
}

var candidateAttributes: UICollectionViewLayoutAttributes?
let proposedContentOffsetCenterX = proposedContentOffset.x + collectionView.bounds.width / 2

for attributes in layoutAttributes {
if attributes.representedElementCategory != .cell {
continue
}

if candidateAttributes == nil {
candidateAttributes = attributes
continue
}

if abs(attributes.center.x - proposedContentOffsetCenterX) < abs(candidateAttributes!.center.x - proposedContentOffsetCenterX) {
candidateAttributes = attributes
}
}

guard let aCandidateAttributes = candidateAttributes else {
return proposedContentOffset
}

var newOffsetX = aCandidateAttributes.center.x - collectionView.bounds.size.width / 2
let offset = newOffsetX - collectionView.contentOffset.x

if (velocity.x < 0 && offset > 0) || (velocity.x > 0 && offset < 0) {
let pageWidth = itemSize.width + minimumLineSpacing
newOffsetX += velocity.x > 0 ? pageWidth : -pageWidth
}

return CGPoint(x: newOffsetX, y: proposedContentOffset.y)
}

override func shouldInvalidateLayout(forBoundsChange _: CGRect) -> Bool {
return true
}

override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let collectionView = self.collectionView,
let superAttributes = super.layoutAttributesForElements(in: rect) else {
return super.layoutAttributesForElements(in: rect)
}

let contentOffset = collectionView.contentOffset
let size = collectionView.bounds.size

let visibleRect = CGRect(x: contentOffset.x, y: contentOffset.y, width: size.width, height: size.height)
let visibleCenterX = visibleRect.midX

guard case let newAttributesArray as [UICollectionViewLayoutAttributes] = NSArray(array: superAttributes, copyItems: true) else {
return nil
}

newAttributesArray.forEach {
let distanceFromCenter = visibleCenterX - $0.center.x
let absDistanceFromCenter = min(abs(distanceFromCenter), self.scaleOffset)
let scale = absDistanceFromCenter * (self.scaleFactor - 1) / self.scaleOffset + 1
$0.transform3D = CATransform3DScale(CATransform3DIdentity, scale, scale, 1)

let alpha = absDistanceFromCenter * (self.alphaFactor - 1) / self.scaleOffset + 1
$0.alpha = alpha
}

return newAttributesArray
}

func configureContentInset() {
guard let collectionView = self.collectionView else {
return
}

let inset = collectionView.bounds.size.width / 2 - itemSize.width / 2
collectionView.contentInset = UIEdgeInsets.init(top: 0, left: inset, bottom: 0, right: inset)
collectionView.contentOffset = CGPoint(x: -inset, y: 0)
}

func resetContentInset() {
guard let collectionView = self.collectionView else {
return
}

collectionView.contentInset = UIEdgeInsets.init(top: 0, left: 0, bottom: 0, right: 0)
}
}

How to make simple pan to dismiss view in iOS

Issue #301

Make it more composable using UIViewController subclass and ThroughView to pass hit events to underlying views.

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
class PanViewController: UIViewController {
var animator = UIViewPropertyAnimator(duration: 0, curve: .easeOut)
lazy var panGR = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_: )))
let slideView = UIView()
let gripView = UIView()
var options: Options = Options()
var didHide: (() -> Void)?
let pullDownVelocity: CGFloat = 70

class Options {
var contentView: UIView = UIView()
var percentHeight: CGFloat = 0.24
}

override func loadView() {
view = ThroughView()
view.translatesAutoresizingMaskIntoConstraints = false
}

override func viewDidLoad() {
super.viewDidLoad()

setup()
}

func setup() {
slideView.layer.cornerRadius = 10
slideView.clipsToBounds = true

gripView.backgroundColor = UIColor.yellow
gripView.layer.cornerRadius = 1

view.addSubview(slideView)
slideView.addSubview(gripView)
slideView.addGestureRecognizer(panGR)

NSLayoutConstraint.on([
slideView.leftAnchor.constraint(equalTo: view.leftAnchor),
slideView.rightAnchor.constraint(equalTo: view.rightAnchor),
slideView.heightAnchor.constraint(equalTo: view.heightAnchor),
slideView.topAnchor.constraint(equalTo: view.bottomAnchor)
])

NSLayoutConstraint.on([
gripView.centerXAnchor.constraint(equalTo: slideView.centerXAnchor),
gripView.topAnchor.constraint(equalTo: slideView.topAnchor, constant: 16),
gripView.widthAnchor.constraint(equalToConstant: 30),
gripView.heightAnchor.constraint(equalToConstant: 2)
])
}

func apply(options: Options) {
self.options.contentView.removeFromSuperview()
slideView.insertSubview(options.contentView, at: 0)

NSLayoutConstraint.on([
options.contentView.leftAnchor.constraint(equalTo: slideView.leftAnchor),
options.contentView.rightAnchor.constraint(equalTo: slideView.rightAnchor),
options.contentView.topAnchor.constraint(equalTo: slideView.topAnchor),
options.contentView.heightAnchor.constraint(equalTo: slideView.heightAnchor, multiplier: options.percentHeight)
])

self.options = options
}

@objc func handlePan(_ gr: UIPanGestureRecognizer) {
switch gr.state {
case .began:
break
case .changed:
break
case .ended:
let velocity = gr.velocity(in: slideView)
if velocity.y > pullDownVelocity {
hide()
}
default:
break
}
}

func show() {
guard let parentView = view.superview else {
return
}

animator = self.makeAnimator()
animator.addAnimations {
self.slideView.transform = CGAffineTransform(
translationX: 0,
y: -parentView.bounds.height * self.options.percentHeight - parentView.safeAreaInsets.bottom
)
}

animator.startAnimation()
}

func hide() {
animator = self.makeAnimator()
animator.addAnimations {
self.slideView.transform = CGAffineTransform.identity
}

animator.addCompletion({ _ in
self.didHide?()
})

animator.startAnimation()
}

func makeAnimator() -> UIViewPropertyAnimator {
return UIViewPropertyAnimator(duration: 0.25, dampingRatio: 1.0)
}
}

class ThroughView: UIView {
override func didMoveToSuperview() {
super.didMoveToSuperview()

guard let superview = superview else {
return
}

NSLayoutConstraint.on([pinEdges(view: superview)])
}

override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
guard let slideView = subviews.first else {
return false
}

return slideView.hitTest(convert(point, to: slideView), with: event) != nil
}
}

Getting started with WWDC 2019

Issue #300

WWDC

Swift 5.1

SwiftUI

Good to know

Code

Combine

Dark Mode

How to style NSButton in AppKit

Issue #297

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let button = NSButton()
button.wantsLayer = true
button.isBordered = false
button.setButtonType(.momentaryChange)
button.attributedTitle = NSAttributedString(
string: "Click me",
attributes: [
NSAttributedString.Key.foregroundColor: NSColor.white,
NSAttributedString.Key.font: NSFont.labelFont(ofSize: 13)
]
button.layer?.backgroundColor = NSColor.orange.cgColor
button.layer?.cornerRadius = 12

activate(
button.anchor.height.equal.to(32),
button.anchor.width.equal.to(100)
)

To make it have native rounded rect

1
2
3
4
button.imageScaling = .scaleProportionallyDown
button.setButtonType(.momentaryPushIn)
button.bezelStyle = .rounded
button.isBordered = true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import AppKit
import Omnia

extension NSButton {
func style(imageName: String) {
image = NSImage(named: NSImage.Name(imageName))
isBordered = false
imageScaling = .scaleProportionallyDown
}

func styleAction(title: String) {
attributedTitle = NSAttributedString(string: title, attributes: [
NSAttributedString.Key.foregroundColor: NSColor(hex: "008B80"),
NSAttributedString.Key.font: NSFont.boldSystemFont(ofSize: 13)
])

isBordered = false
setButtonType(.momentaryChange)
}
}

Read more

How to add indicator under tab bar buttons in iOS

Issue #288

selectionIndicatorImage

Use this property to specify a custom selection image. Your image is rendered on top of the tab bar but behind the contents of the tab bar item itself. The default value of this property is nil, which causes the tab bar to apply a default highlight to the selected item

Custom UITabBar or UITabBarController

  • Hide existing tabBar tabBar.isHidden = true
  • Position custom buttons
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
import UIKit

class CustomTabBarController: UITabBarController {
private let bar = UIView()
private var buttons = [UIButton]()

override var viewControllers: [UIViewController]? {
didSet {
populate(viewControllers: viewControllers)
}
}

override func viewDidLoad() {
super.viewDidLoad()

setup()
}

private func setup() {
tabBar.isHidden = true
bar.backgroundColor = R.color.background
view.addSubview(bar)
NSLayoutConstraint.on([
bar.leftAnchor.constraint(equalTo: view.leftAnchor),
bar.rightAnchor.constraint(equalTo: view.rightAnchor),
bar.bottomAnchor.constraint(equalTo: view.bottomAnchor),
bar.topAnchor.constraint(equalTo: tabBar.topAnchor)
])
}

private func populate(viewControllers: [UIViewController]) {
buttons.forEach {
$0.removeFromSuperview()
}

buttons = viewControllers.map({
let button = UIButton()
button.setImage($0.tabBarItem.image, for: .normal)
bar.addSubview(button)
return button
})

view.setNeedsLayout()
}

override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()

let padding: CGFloat = 20
let buttonSize = CGSize(width: 30, height: 44)
let width = view.bounds.width - padding * 20

for (index, button) in buttons.enumerated() {
button.center = CGPoint(x: bar.center.x, y: bar.frame.height/2)
}
}
}

Handle UITabBarControllerDelegate

  • Override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem)
  • tabBar.subviews contains 1 private UITabBarBackground and many private UITabBarButton
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
import UIKit

class CustomTabBarController: UITabBarController {
let indicator: UIView = {
let view = UIView()
view.backgroundColor = R.color.primary
view.frame.size = CGSize(width: 32, height: 2)
view.layer.cornerRadius = 1

return view
}()

override func viewDidLoad() {
super.viewDidLoad()

tabBar.addSubview(indicator)
self.delegate = self
}

private func animate(index: Int) {
let buttons = tabBar.subviews
.filter({ String(describing: $0).contains("Button") })

guard index < buttons.count else {
return
}

let selectedButton = buttons[index]
UIView.animate(
withDuration: 0.25,
delay: 0,
options: .curveEaseInOut,
animations: {
let point = CGPoint(
x: selectedButton.center.x,
y: selectedButton.frame.maxY - 1
)

self.indicator.center = point
},
completion: nil
)
}

override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()

animate(index: selectedIndex)
}
}

extension CustomTabBarController: UITabBarControllerDelegate {
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
guard
let items = tabBar.items,
let index = items.firstIndex(of: item)
else {
return
}

animate(index: index)
}
}

In iOS 13, we need to use viewDidAppear

1
2
3
4
5
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)

moveIndicator(index: selectedIndex, animated: false)
}

Getting to know some pragmatic programming language features

Issue #270

As you know, in the Pragmatic Programmer, section Your Knowledge Portfolio, it is said that

Learn at least one new language every year. Different languages solve the same problems in different ways. By learning several different approaches, you can help broaden your thinking and avoid getting stuck in a rut. Additionally, learning many languages is far easier now, thanks to the wealth of freely available software on the Internet

I see learning programming languages as a chance to open up my horizon and learn some new concepts. It also encourage good habit like immutability, composition, modulation, …

I’d like to review some of the features of all the languages I have played with. Some are useful, some just make me interested or say “wow”

Curly braces

Each language can have its own style of grouping block of code, but I myself like the curly braces the most, which are cute :]

Some like C, Java, Swift, … use curly braces

Swift

init(total: Double, taxPct: Double) {
  self.total = total
  self.taxPct = taxPct
  subtotal = total / (taxPct + 1)
}

Some like Haskell, Python, … use indentation

Haskell

bmiTell :: (RealFloat a) => a -> String  
bmiTell bmi  
    | bmi <= 18.5 = "You're underweight, you emo, you!"  
    | bmi <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!"  
    | bmi <= 30.0 = "You're fat! Lose some weight, fatty!"  
    | otherwise   = "You're a whale, congratulations!"

Some like Elixir use keyword list

ELixir

if false, do: :this, else: :that

Named parameter

Language like Objective C, Swift offer named parameter, which make it easier to reason about a function call

func sayHello(to person: String, and anotherPerson: String) -> String {
    return "Hello \(person) and \(anotherPerson)!"
}

Explicit type

Language like C, Swift, Java, … have type information in parameter and in return, which make it easier to reason about a function call

Swift

func sayHello(personName: String, alreadyGreeted: Bool) -> String {
    if alreadyGreeted {
        return sayHelloAgain(personName)
    } else {
        return sayHello(personName)
    }
}

List comprehension

Languages like Haskell, Python, Elixir, support list comprehension

Elixir

iex> for n <- [1, 2, 3, 4], do: n * n
[1, 4, 9, 16]

First class function

I enjoy functional programming, so first class function support in Javascript, Swift, Haskell, Elixir, … really make me happy

Haskell

zipWith' (*) (replicate 5 2) [1..]

Curry

Currying is the technique of translating the evaluation of a function that takes multiple arguments (or a tuple of arguments) into evaluating a sequence of functions, each with a single argument (partial application)

Language like Swift 2, Haskell, … have curry by default. Some like Javascript can use libraries (Lodash, …) to achieve this. In Haskell, every function officially only takes one parameter.

In Swift 3, curry was removed :(

Haskell

1
2
multThree :: (Num a) => a -> a -> a -> a  
multThree x y z = x * y * z

By calling functions with too few parameters, we’re creating new functions on the fly.

Javascript

1
2
3
4
5
6
7
8
9
var curry = require('lodash.curry');
var map = curry(function(f, ary) {
return ary.map(f);
});
var getChildren = function(x) {
return x.childNodes;
};

var allTheChildren = map(getChildren);

Pattern matching

I find pattern matching as a better way around if else statement

Swift supports pattern matching in switch statement

Swift

1
2
3
4
5
6
7
8
9
10
11
enum Trades {
case Buy(stock: String, amount: Int, stockPrice: Float)
case Sell(stock: String, amount: Int, stockPrice: Float)
}
let aTrade = Trades.Buy(stock: "APPL", amount: 200, stockPrice: 115.5)
switch aTrade {
case .Buy(let stock, let amount, _):
process(stock, amount)
case .Sell(let stock, let amount, _):
process(stock, amount * -1)
}

Some like Haskell, Elixir, … also pattern matches on function name, which makes it work great for recursion

Haskell

sayMe :: (Integral a) => a -> String  
sayMe 1 = "One!"  
sayMe 2 = "Two!"  
sayMe 3 = "Three!"  
sayMe 4 = "Four!"  
sayMe 5 = "Five!"  
sayMe x = "Not between 1 and 5"  


map _ []     = []
map f (x:xs) = f x : map f xs

In Elixir, the = operator is actually a match operator

Elixir

iex> x = 1
1
iex> x
1
iex> 1 = x
1
iex> 2 = x
** (MatchError) no match of right hand side value: 1

Recursion

Some language like Haskell, Elixir, … don’t use loop, they use recursion with performance in mind, no overflow.

Haskell

length' :: (Num b) => [a] -> b  
length' [] = 0  
length' (_:xs) = 1 + length' xs

Laziness

Some languages support infinite collection, thanks to their laziness.

Haskell is lazy, if you map something over a list several times and filter it several times, it will only pass over the list once

Haskell

largestDivisible :: (Integral a) => a  
largestDivisible = head (filter p [100000,99999..])  
    where p x = x `mod` 3829 == 0

Elixir defines the concept of Eager with Enum and Lazy with Stream

Elixir

1..100_000 |> Enum.map(&(&1 * 3)) |> Enum.filter(odd?) |> Enum.sum

Custom operator

Elixir is famous for its pipe |> operator

The |> symbol used in the snippet above is the pipe operator: it simply takes the output from the expression on its left side and passes it as the first argument to the function call on its right side

Elixir

1..100_000 |> Enum.map(&(&1 * 3)) |> Enum.filter(odd?) |> Enum.sum

Haskell often takes advantage of this custom -: operator Haskell

x -: f = f x  

(0,0) -: landLeft 1 -: landRight 1 -: landLeft 2

Functor, Applicative Functor, Monoid, Monad

I really like enjoy Haskell because of these typeclasses. It realizes common pattern (map, apply, join, bind, …) with comptutational context. It really enlightens me when I find that function is a Monad as well (you should read the Reader monad)

Haskell

instance Monad Maybe where  
    return x = Just x  
    Nothing >>= f = Nothing  
    Just x >>= f  = f x  
    fail _ = Nothing  

landLeft :: Birds -> Pole -> Maybe Pole  
landLeft n (left,right)  
    | abs ((left + n) - right) < 4 = Just (left + n, right)  
    | otherwise                    = Nothing  

landRight :: Birds -> Pole -> Maybe Pole  
landRight n (left,right)  
    | abs (left - (right + n)) < 4 = Just (left, right + n)  
    | otherwise                    = Nothing

ghci> return (0,0) >>= landLeft 1 >>= banana >>= landRight 1  
Nothing

List comprehension in Haskell is just syntactic sugar for using lis as Monad Haskell

ghci> [ (n,ch) | n <- [1,2], ch <- ['a','b'] ]  
[(1,'a'),(1,'b'),(2,'a'),(2,'b')]

Swift

enum Result<T> {
    case Value(T)
    case Error(NSError)
}

extension Result {
    func map<U>(f: T -> U) -> Result<U> {
        switch self {
            case let .Value(value):
                return Result<U>.Value(f(value))
            case let .Error(error):
                return Result<U>.Error(error)
        }
    }
}

extension Result {
    static func flatten<T>(result: Result<Result<T>>) -> Result<T> {
        switch result {
            case let .Value(innerResult):
                return innerResult
            case let .Error(error):
                return Result<T>.Error(error)
        }
    }
}

extension Result {
    func flatMap<U>(f: T -> Result<U>) -> Result<U> {
        return Result.flatten(map(f))
    }
}

Trait and mixin

Languages like Scala, … support trait

Similar to interfaces in Java, traits are used to define object types by specifying the signature of the supported methods. Unlike Java, Scala allows traits to be partially implemented; i.e. it is possible to define default implementations for some methods

Scala

trait Similarity {
  def isSimilar(x: Any): Boolean
  def isNotSimilar(x: Any): Boolean = !isSimilar(x)
}

class Point(xc: Int, yc: Int) extends Similarity {
  var x: Int = xc
  var y: Int = yc
  def isSimilar(obj: Any) =
    obj.isInstanceOf[Point] &&
    obj.asInstanceOf[Point].x == x
}

Swift can uses Protocol Extension to achieve trait

Swift

protocol GunTrait {
    func shoot() -> String {
        return "Shoot"
    }
}

protocol RenderTrait {
    func render() -> String {
        return "Render"
    }
}

struct Player: GameObject, AITrait, GunTrait, RenderTrait, HealthTrait {

}

Ruby supports Mixin via Module

Ruby

module Greetings
  def hello
    puts "Hello!"
  end

  def bonjour
    puts "Bonjour!"
  end

  def hola
    puts "Hola!"
  end
end

class User
  include Greetings
end

Delegate property

There are certain common kinds of properties that would be very nice to implement once and for all like lazy, observable and storing. An example is in Kotlin

class Delegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, thank you for delegating '${property.name}' to me!"
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$value has been assigned to '${property.name}' in $thisRef.")
    }
}

Where to go from here

Hope you find something interesting. Each language has its own pros and is designed for specific purpose. So no list will be enough to cover them all.

To take a quick peek into other programming languages, I find Learn in One Video by Derek very helpful.

There are things that intrigue us every day like Swift initialization rule make it explicit when using initializer, Go goroutine and channel for concurrent code, Elixir process for easy concurrent and message communication. You’ll be amazed by how process encapsulates state, Haskell data type encourages immutability and thread safe code, Elixir macro for great extension of the language. The best way to to learn is to use and dive into the languages often.

May your code continue to compile.

While you are here, you may like my other posts


Updated at 2020-12-05 05:44:54

How to get running window informations in macOS

Issue #243

From https://developer.apple.com/documentation/coregraphics/1455137-cgwindowlistcopywindowinfo

Generates and returns information about the selected windows in the current user session.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
struct MyWindowInfo {
let frame: CGRect
let name: String
let pid: Int
let number: Int

init?(json: [String: Any]) {
guard let pid = json["kCGWindowOwnerPID"] as? Int else {
return nil
}

guard let name = json["kCGWindowOwnerName"] as? String else {
return nil
}

guard let rect = json["kCGWindowBounds"] as? [String: Any] else {
return nil
}

guard let x = rect["X"] as? CGFloat else {
return nil
}

guard let y = rect["Y"] as? CGFloat else {
return nil
}

guard let height = rect["Height"] as? CGFloat else {
return nil
}

guard let width = rect["Width"] as? CGFloat else {
return nil
}

guard let number = json["kCGWindowNumber"] as? Int else {
return nil
}

self.pid = pid
self.name = name
self.number = number
self.frame = CGRect(x: x, y: y, width: width, height: height)
}
}

guard let jsons = CGWindowListCopyWindowInfo(.optionAll, kCGNullWindowID) as? [[String: Any]] else {
return
}

let infos = jsons.compactMap({ MyWindowInfo(json: $0) })

How to shake NSView in macOS

Issue #233

Animation on macOS using CAAnimation

Shake

1
2
3
4
5
6
7
8
9
10
let midX = box.layer?.position.x ?? 0
let midY = box.layer?.position.y ?? 0

let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.06
animation.repeatCount = 4
animation.autoreverses = true
animation.fromValue = CGPoint(x: midX - 10, y: midY)
animation.toValue = CGPoint(x: midX + 10, y: midY)
box.layer?.add(animation, forKey: "position")

Animation on macOS using NSAnimationContext

Wiggle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
NSAnimationContext.runAnimationGroup({ context in
let animation = CAKeyframeAnimation(keyPath: "transform")
animation.beginTime = CACurrentMediaTime() + 5.0
animation.duration = 0.1
animation.autoreverses = true
let wobbleAngle: CGFloat = 0.08
animation.values = [
NSValue(caTransform3D: CATransform3DMakeRotation(wobbleAngle, 0.0, 0.0, 1.0)),
NSValue(caTransform3D: CATransform3DMakeRotation(-wobbleAngle, 0.0, 0.0, 1.0))
]
view.layer?.add(animation, forKey: "transform")
}, completionHandler: {
self.makeAnimation(view: view)
})

Animation on iOS using UIView animation block

1
2
3
4
5
6
7
8
extension UIView {
func shake() {
self.transform = CGAffineTransform(translationX: 16, y: 0)
UIView.animate(withDuration: 0.4, delay: 0, usingSpringWithDamping: 0.2, initialSpringVelocity: 1, options: .curveEaseInOut, animations: {
self.transform = CGAffineTransform.identity
}, completion: nil)
}
}

How to use CAReplicatorLayer to make activity indicator in iOS

Issue #230

CAReplicatorLayer is a layer that creates a specified number of sublayer copies with varying geometric, temporal, and color transformations

Here we use instanceTransform which applies transformation matrix around the center of the replicator layer

Below is how we use replicatorLayer to replicate lots of line and rotate them around the center.

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
let replicatorLayer = CAReplicatorLayer()
let animation = CABasicAnimation(keyPath: #keyPath(CALayer.opacity))

let line = CALayer()
let lineCount: Int = 12
let duration: TimeInterval = 1.0
let lineSize: CGSize = CGSize(width: 20, height: 6)
let lineColor: UIColor = UIColor.darkGray

let angle = CGFloat.pi * 2 / CGFloat(lineCount)
let rotation = CATransform3DMakeRotation(angle, 0, 0, 1.0)

replicatorLayer.instanceTransform = rotation
replicatorLayer.instanceCount = lineCount
replicatorLayer.instanceDelay = duration / TimeInterval(lineCount)

line.backgroundColor = lineColor.cgColor
line.frame.size = lineSize
line.cornerRadius = lineSize.height / 2

animation.fromValue = 1.0
animation.toValue = 0.0
animation.repeatCount = Float.greatestFiniteMagnitude
animation.timingFunction = CAMediaTimingFunction(name: .linear)
animation.duration = duration

replicatorLayer.addSublayer(line)
layer.addSublayer(replicatorLayer)

// x:
// y: half the height, changing affects rotation of lines
line.position = CGPoint(x: 48, y: 75)

line.add(animation, forKey: nil)

Pay attention to position of the line. The larger the x, the closer to center. y should be half the height of the replicator layer size, changing it affects the skewness of the line.

indicator

How to do rotation for CALayer in iOS

Issue #229

Use keypath

1
2
3
4
5
6
let animation = CASpringAnimation(keyPath: #keyPath(CALayer.transform))
animation.fromValue = 0
animation.valueFunction = CAValueFunction(name: CAValueFunctionName.rotateZ)
animation.timingFunction = CAMediaTimingFunction(name: .easeIn)

animation.toValue = CGFloat.pi / 4

Avoid setting frame many times

Otherwise, frame is the box that covers the rotation transform, and backgroundColor now fills that huge box

abc

1
2
3
4
5
6
7
8
9
10
11
12
override func layoutSubviews() {
super.layoutSubviews()

guard line.frame.width <= 0 else {
return
}

line.backgroundColor = UIColor.red.cgColor
line.cornerRadius = 3
line.frame.size = CGSize(width: bounds.width*0.6, height: 6)
line.position = layer.position
}

Auto Layout

Avoid using Auto Layout for the rotated view

How to not use isRemovedOnCompletion for CAAnimation in iOS

Issue #228

CAAnimation is about presentation layer, after animation completes, the view snaps back to its original state. If we want to keep the state after animation, then the wrong way is to use CAMediaTimingFillMode.forward and isRemovedOnCompletion

Animation never ends

forwards https://developer.apple.com/documentation/quartzcore/camediatimingfillmode/1427658-forwards

The receiver remains visible in its final state when the animation is completed.

isRemovedOnCompletion

https://developer.apple.com/documentation/quartzcore/caanimation/1412458-isremovedoncompletion

When true, the animation is removed from the target layer’s animations once its active duration has passed. Defaults to true.

1
2
layer.fillMode = .forwards
animation. isRemovedOnCompletion = false

This is to tell the animation to never ends and keep its last presentation state. Wrong approach ❗️

Set final state before calling animation

The presentation state is just for animation, the source of truth lies in the layer itself. We need to set the final state before calling animation

1
2
3
4
let animation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.strokeEnd))

shapeLayer.strokeEnd = 1.0
shapeLayer.add(animation, forKey: "")