How to handle keyboard for UITextField in scrolling UIStackView in iOS

Issue #329

Firstly, to make UIStackView scrollable, embed it inside UIScrollView. Read How to embed UIStackView inside UIScrollView in iOS

It’s best to listen to keyboardWillChangeFrameNotification as it contains frame changes for Keyboard in different situation like custom keyboard, languages.

Posted immediately prior to a change in the keyboard’s frame.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class KeyboardHandler {
let scrollView: UIScrollView
let stackView: UIStackView
var observer: AnyObject?
var keyboardHeightConstraint: NSLayoutConstraint!

struct Info {
let frame: CGRect
let duration: Double
let animationOptions: UIView.AnimationOptions
}

init(scrollView: UIScrollView, stackView: UIStackView) {
self.scrollView = scrollView
self.stackView = stackView
}
}

To make scrollView scroll beyond its contentSize, we can change its contentInset.bottom. Another way is to add a dummy view with certain height to UIStackView and alter its NSLayoutConstraint constant

We can’t access self inside init, so it’s best to have setup function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func setup() {
let space = UIView()
keyboardHeightConstraint = space.heightAnchor.constraint(equalToConstant: 0)
NSLayoutConstraint.on([keyboardHeightConstraint])
stackView.addArrangedSubview(spa
observer = NotificationCenter.default.addObserver(
forName: UIResponder.keyboardWillChangeFrameNotification,
object: nil,
queue: .main,
using: { [weak self] notification in
self?.handle(notification)
}
)
}

Convert Notification to a convenient Info struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func convert(notification: Notification) -> Info? {
guard
let frameValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] NSValue,
let durationotification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber,
let raw = notification.userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] NSNumber
else {
return nil

return Info(
frame: frameValue.cgRectValue,
duration: duration.doubleValue,
animationOptions: UIView.AnimationOptions(rawValue: raw.uintValue)
)
}

Then we can compare with UIScreen to check if Keyboard is showing or hiding

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func handle(_ notification: Notification) {
guard let info = convert(notification: notification) else {
return

let isHiding = info.frame.origin.y == UIScreen.main.bounds.height
keyboardHeightConstraint.constant = isHiding ? 0 : info.frame.hei
UIView.animate(
withDuration: info.duration,
delay: 0,
options: info.animationOptions,
animations: {
self.scrollView.layoutIfNeeded()
self.moveTextFieldIfNeeded(info: info)
}, completion: nil)
}

To move UITextField we can use scrollRectToVisible(_:animated:) but we have little control over how much we want to scroll

This method scrolls the content view so that the area defined by rect is just visible inside the scroll view. If the area is already visible, the method does nothing.

Another way is to check if keyboard overlaps UITextField. To do that we use convertRect:toView: with nil target so it uses window coordinates. Since keyboard frame is always relative to window, we have frames in same coordinate space.

Converts a rectangle from the receiver’s coordinate system to that of another view.

rect: A rectangle specified in the local coordinate system (bounds) of the receiver.
view: The view that is the target of the conversion operation. If view is nil, this method instead converts to window base coordinates. Otherwise, both view and the receiver must belong to the same UIWindow object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func moveTextFieldIfNeeded(info: Info) {
guard let input = stackView.arrangedSubviews
.compactMap({ $0 as? UITextField })
.first(where: { $0.isFirstResponder })
else {
return

let inputFrame = input.convert(input.bounds, to: nil)
if inputFrame.intersects(info.frame) {
scrollView.setContentOffset(CGPoint(x: 0, y: inputFrame.height), animated: true)
} else {
scrollView.setContentOffset(.zero, animated: true)
}
}

Move up the entire view

For simplicity, we can move up the entire view

1
2
3
4
5
6
7
8
9
10
11
12
13
func move(info: Info) {
let isHiding = info.frame.origin.y == UIScreen.main.bounds.height
let moveUp = CGAffineTransform(translationX: 0, y: -info.frame.height)

switch (view.transform, isHiding) {
case (.identity, false):
view.transform = moveUp
case (moveUp, true):
view.transform = .identity
default:
break
}
}

Prefer willShow and willHide

There ‘s an edge case with the above switch on view.transform and isHiding with one time verification sms code, which make it into the correct case handling. It’s safe to just set view.transform depending on show with willHide and willShow

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

class KeyboardHandler {
let view: UIView
var observerForWillShow: AnyObject?
var observerForWillHide: AnyObject?
var keyboardHeightConstraint: NSLayoutConstraint!

struct Info {
let frame: CGRect
let duration: Double
let animationOptions: UIView.AnimationOptions
}

init(view: UIView) {
self.view = view
}

func setup() {
observerForWillShow = NotificationCenter.default.addObserver(
forName: UIResponder.keyboardWillShowNotification,
object: nil,
queue: .main,
using: { [weak self] notification in
self?.handle(notification, show: true)
}
)

observerForWillHide = NotificationCenter.default.addObserver(
forName: UIResponder.keyboardWillHideNotification,
object: nil,
queue: .main,
using: { [weak self] notification in
self?.handle(notification, show: false)
}
)
}

func handle(_ notification: Notification, show: Bool) {
guard let info = convert(notification: notification) else {
return
}

UIView.animate(
withDuration: info.duration,
delay: 0,
options: info.animationOptions,
animations: {
self.move(info: info, show: show)
}, completion: nil)
}

func move(info: Info, show: Bool) {
let moveUp = CGAffineTransform(translationX: 0, y: -info.frame.height)
if show {
view.transform = moveUp
} else {
view.transform = .identity
}
}

func convert(notification: Notification) -> Info? {
guard
let frameValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue,
let duration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber,
let raw = notification.userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber
else {
return nil
}

return Info(
frame: frameValue.cgRectValue,
duration: duration.doubleValue,
animationOptions: UIView.AnimationOptions(rawValue: raw.uintValue)
)
}
}

Read more


Updated at 2020-07-06 07:09:07

How to make simple form validator in Swift

Issue #328

Sometimes we want to validate forms with many fields, for example name, phone, email, and with different rules. If validation fails, we show error message.

We can make simple Validator and Rule

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
class Validator {
func validate(text: String, with rules: [Rule]) -> String? {
return rules.compactMap({ $0.check(text) }).first
}

func validate(input: InputView, with rules: [Rule]) {
guard let message = validate(text: input.textField.text ?? "", with: rules) else {
input.messageLabel.isHidden = true
return
}

input.messageLabel.isHidden = false
input.messageLabel.text = message
}
}

struct Rule {
// Return nil if matches, error message otherwise
let check: (String) -> String?

static let notEmpty = Rule(check: {
return $0.isEmpty ? "Must not be empty" : nil
})

static let validEmail = Rule(check: {
let regex = #"[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,64}"#

let predicate = NSPredicate(format: "SELF MATCHES %@", regex)
return predicate.evaluate(with: $0) ? nil : "Must have valid email"
})

static let countryCode = Rule(check: {
let regex = #"^\+\d+.*"#

let predicate = NSPredicate(format: "SELF MATCHES %@", regex)
return predicate.evaluate(with: $0) ? nil : "Must have prefix country code"
})
}

Then we can use very expressively

1
2
let validator = Validator()
validator.validate(input: inputView, with: [.notEmpty, .validEmail])

Then a few tests to make sure it works

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class ValidatorTests: XCTestCase {
let validator = Validator()

func testEmpty() {
XCTAssertNil(validator.validate(text: "a", with: [.notEmpty]))
XCTAssertNotNil(validator.validate(text: "", with: [.notEmpty]))
}

func testEmail() {
XCTAssertNil(validator.validate(text: "onmyway133@gmail.com", with: [.validEmail]))
XCTAssertNotNil(validator.validate(text: "onmyway133", with: [.validEmail]))
XCTAssertNotNil(validator.validate(text: "onmyway133.com", with: [.validEmail]))
}

func testCountryCode() {
XCTAssertNil(validator.validate(text: "+47 11 222 333", with: [.countryCode]))
XCTAssertNotNil(validator.validate(text: "11 222 333", with: [.countryCode]))
XCTAssertNotNil(validator.validate(text: "47 11 222 333", with: [.countryCode]))
}
}

allSatisfy

To check if all rules are ok, we can use reduce

1
2
3
func check(text: String, with rules: [Rule]) -> Bool {
return rules.allSatisfy({ $0.check(text).isOk })
}

Or more concisely, use allSatisfy

1
2
3
4

func check(text: String, with rules: [Rule]) -> Bool {
return rules.allSatisfy({ $0.check(text).isOk })
}

How to organise test files

Issue #327

In terms of tests, we usually have files for unit test, UI test, integeration test and mock.

Out of sight, out of mind.

Unit tests are for checking specific functions and classes, it’s more convenient to browse them side by side with source file. For example in Javascript, Kotlin and Swift

1
2
3
index.js
index.test.js
index.mock.js
1
2
3
LocationManager.kt
LocationManager.mock.kt
LocationManager.test.kt
1
2
3
BasketHandler.swift
BasketHandler.mock.swift
BasketHandler.test.swift

Integration tests check features or sub features, and may cover many source files, it’s better to place them in feature folders

1
2
3
4
5
6
7
8
9
10
11
- Features
- Cart
- Sources
- Tests
- Cart.test.swift
- Validator.test.swift
- Profile
- Sources
- Tests
- Updater.test.swift
- AvatarUploader.test.swift

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 use media query in CSS

Issue #314

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<meta name="viewport" content="width=device-width, initial-scale=1.0">

@media only screen and (max-width: 600px) {
div.container {
margin: 0 auto;
width: 70%;
margin-top: 60%;
}
}

body {
background-image: url("../images/wallpaper.png");
background-position: top right;
background-attachment: fixed;
background-size: cover;
}

Links for WWDC

Issue #313

iOS 10

UserNotifications

Push user-facing notifications to the user’s device from a server, or generate them locally from your app.

UIViewPropertyAnimator

A class that animates changes to views and allows the dynamic modification of those animations.

NSPersistentContainer

A container that encapsulates the Core Data stack in your app.

UIFeedbackGenerator

The abstract superclass for all feedback generators.

iOS 10.3

SKStoreReviewController

An object that controls the process of requesting App Store ratings and reviews from users.

iOS 11

safeAreaLayoutGuide

The layout guide representing the portion of your view that is unobscured by bars and other content.

CoreML

Integrate machine learning models into your app.

Vision

Apply computer vision algorithms to perform a variety of tasks on input images and video.

ARKit

Integrate iOS device camera and motion features to produce augmented reality experiences in your app or game.

DeviceCheck

Access per-device, per-developer data that your associated server can use in its business logic.

Drag and Drop

Bring drag and drop to your app by using interaction APIs with your views.

CoreNFC

Detect NFC tags and read messages that contain NDEF data.

maskedCorners

Animatable corner radius

iOS 12

AuthenticationServices

Make it easy for users to log into apps and services.

iOS 13

SwiftUI

Declare the user interface and behavior for your app on every platform.

Combine

Customize handling of asynchronous events by combining event-processing operators.

CryptoKit

Perform cryptographic operations securely and efficiently.

UISearchTextField

Expose UISearchTextField on UISearchBar

MetricKit

➕CoreSVG

What's new in iOS

Issue #313

iOS 10

UserNotifications

Push user-facing notifications to the user’s device from a server, or generate them locally from your app.

UIViewPropertyAnimator

A class that animates changes to views and allows the dynamic modification of those animations.

NSPersistentContainer

A container that encapsulates the Core Data stack in your app.

UIFeedbackGenerator

The abstract superclass for all feedback generators.

iOS 10.3

SKStoreReviewController

An object that controls the process of requesting App Store ratings and reviews from users.

iOS 11

safeAreaLayoutGuide

The layout guide representing the portion of your view that is unobscured by bars and other content.

CoreML

Integrate machine learning models into your app.

Vision

Apply computer vision algorithms to perform a variety of tasks on input images and video.

ARKit

Integrate iOS device camera and motion features to produce augmented reality experiences in your app or game.

DeviceCheck

Access per-device, per-developer data that your associated server can use in its business logic.

Drag and Drop

Bring drag and drop to your app by using interaction APIs with your views.

CoreNFC

Detect NFC tags and read messages that contain NDEF data.

maskedCorners

Animatable corner radius

iOS 12

AuthenticationServices

Make it easy for users to log into apps and services.

iOS 13

SwiftUI

Declare the user interface and behavior for your app on every platform.

Combine

Customize handling of asynchronous events by combining event-processing operators.

CryptoKit

Perform cryptographic operations securely and efficiently.

UISearchTextField

Expose UISearchTextField on UISearchBar

MetricKit

How to use new APIs in iOS

Issue #313

iOS 10

UserNotifications

Push user-facing notifications to the user’s device from a server, or generate them locally from your app.

UIViewPropertyAnimator

A class that animates changes to views and allows the dynamic modification of those animations.

NSPersistentContainer

A container that encapsulates the Core Data stack in your app.

UIFeedbackGenerator

The abstract superclass for all feedback generators.

iOS 10.3

SKStoreReviewController

An object that controls the process of requesting App Store ratings and reviews from users.

iOS 11

safeAreaLayoutGuide

The layout guide representing the portion of your view that is unobscured by bars and other content.

CoreML

Integrate machine learning models into your app.

Vision

Apply computer vision algorithms to perform a variety of tasks on input images and video.

ARKit

Integrate iOS device camera and motion features to produce augmented reality experiences in your app or game.

DeviceCheck

Access per-device, per-developer data that your associated server can use in its business logic.

Drag and Drop

Bring drag and drop to your app by using interaction APIs with your views.

CoreNFC

Detect NFC tags and read messages that contain NDEF data.

maskedCorners

Animatable corner radius

iOS 12

AuthenticationServices

Make it easy for users to log into apps and services.

iOS 13

SwiftUI

Declare the user interface and behavior for your app on every platform.

Combine

Customize handling of asynchronous events by combining event-processing operators.

CryptoKit

Perform cryptographic operations securely and efficiently.

UISearchTextField

Expose UISearchTextField on UISearchBar

MetricKit

➕CoreSVG

What is new in iOS

Issue #313

iOS 10

UserNotifications

Push user-facing notifications to the user’s device from a server, or generate them locally from your app.

UIViewPropertyAnimator

A class that animates changes to views and allows the dynamic modification of those animations.

NSPersistentContainer

A container that encapsulates the Core Data stack in your app.

UIFeedbackGenerator

The abstract superclass for all feedback generators.

iOS 10.3

SKStoreReviewController

An object that controls the process of requesting App Store ratings and reviews from users.

iOS 11

safeAreaLayoutGuide

The layout guide representing the portion of your view that is unobscured by bars and other content.

CoreML

Integrate machine learning models into your app.

Vision

Apply computer vision algorithms to perform a variety of tasks on input images and video.

ARKit

Integrate iOS device camera and motion features to produce augmented reality experiences in your app or game.

DeviceCheck

Access per-device, per-developer data that your associated server can use in its business logic.

Drag and Drop

Bring drag and drop to your app by using interaction APIs with your views.

CoreNFC

Detect NFC tags and read messages that contain NDEF data.

maskedCorners

Animatable corner radius

iOS 12

AuthenticationServices

Make it easy for users to log into apps and services.

iOS 13

SwiftUI

Declare the user interface and behavior for your app on every platform.

Combine

Customize handling of asynchronous events by combining event-processing operators.

CryptoKit

Perform cryptographic operations securely and efficiently.

UISearchTextField

Expose UISearchTextField on UISearchBar

MetricKit

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

Support IP handover in rtpproxy for VoIP applications

Issue #304

If you do VoIP applications, especially with open sources like pjsip, you may encounter kamalio and rtpproxy to serve SIP requests. Due to limitation of NAT traversals, rtpproxy is needed to work around NAT. All SIP handshake requests go through a proxy server, but rtpproxy can also relay voice, video or any RTP stream of data. When I played with rtpproxy, it was before version 2.0 and I need to handle IP handover. This refers to the scenario when user switches between different network, for example from Wifi to 4G and they get new IP. Normally this means ending in the SIP call, but the expectation is that we can retry and continue the call if possible for users.

That’s why I forked rtpproxy and add IP handover support. You can check the GitHub repo at rtpproxy.

Use src_cnt to track the number of consecutive packets from different address. When this number exceeds THRESHOLD (10 for RTP and 2 for RTCP), I switch to this new address

This way

  • Client can ALWAYS change IP when he switches from 3G to Wifi, or from this Wifi hotspot to another

  • There’s no chance for attack, unless attacker sends > 10 (RTP THRESHOLD) packets in 20ms (supposed my client sends packets every 20ms)

This idea is borrowed from http://www.pjsip.org/pjmedia/docs/html/group__PJMEDIA__CONFIG.htm

There is a macro PJMEDIA_RTP_NAT_PROBATION_CNT. Basically, it is

“See if source address of RTP packet is different than the configured address, and switch RTP remote address to source packet address after several consecutive packets have been received.”

Mobile clients now change IP frequently, from these hotspots to those. So if rtpproxy can support this feature, it would be nicer.

Take a look at https://github.com/onmyway133/rtpproxy/blob/master/rtpp_session.h

// IP Handover Count how many consecutive different packets are received, 0 is for callee, 1 is for caller    unsigned int src_count[2];

And how it actions in https://github.com/onmyway133/rtpproxy/blob/master/main.c

static void
rxmit_packets(struct cfg *cf, struct rtpp_session *sp, int ridx,
  double dtime)
{
    int ndrain, i, port;
    struct rtp_packet *packet = NULL;

/* Repeat since we may have several packets queued on the same socket */
    for (ndrain = 0; ndrain < 5; ndrain++) {
 if (packet != NULL)
     rtp_packet_free(packet);

packet = rtp_recv(sp->fds[ridx]);
 if (packet == NULL)
     break;
 packet->laddr = sp->laddr[ridx];
 packet->rport = sp->ports[ridx];
 packet->rtime = dtime;

i = 0;
 // IP Handover do not need canupdate
 // Use src_count
 if (sp->addr[ridx] != NULL) {
     /* Check that the packet is authentic, drop if it isn't */
     if (sp->asymmetric[ridx] == 0) {
  /*
  if (memcmp(sp->addr[ridx], &packet->raddr, packet->rlen) != 0) {
      if (sp->canupdate[ridx] == 0) {
   //
   // Continue, since there could be good packets in
   // queue.
   //
   continue;
      }

      // Signal that an address has to be updated
      rtpp_log_write(RTPP_LOG_ERR, cf->glog, "IP Handover Set i 1st ridx %d",ridx);
      i = 1;
  } else if (sp->canupdate[ridx] != 0 &&
    sp->last_update[ridx] != 0 &&
    dtime - sp->last_update[ridx] > UPDATE_WINDOW) 
  {
      sp->canupdate[ridx] = 0;
      rtpp_log_write(RTPP_LOG_ERR, cf->glog, "IP Handover Set canupdate to 0 1st ridx %d",ridx);
  }
  */

  if (memcmp(sp->addr[ridx], &packet->raddr, packet->rlen) == 0) { 
   sp->src_count[ridx] = 0;
  } 
  else {
   sp->src_count[ridx]++;
   // IP Handover RTCP packet sends at larger interval, so must use smaller THRESHOLD
   // Check to see if port is odd or even
   if(sp->ports[ridx] % 2 == 0) {
    if(sp->src_count[ridx] >= 10) {
     i = 1;
    } 
   }
   else {
    if(sp->src_count[ridx] >= 2) {
     i = 1;
    }
   }

  }

} else {
  /*
   * For asymmetric clients don't check
   * source port since it may be different.
   */
  rtpp_log_write(RTPP_LOG_ERR, cf->glog, "IP Handover We are in asymmetric ridx %d",ridx);
  if (!ishostseq(sp->addr[ridx], sstosa(&packet->raddr)))
      /*
       * Continue, since there could be good packets in
       * queue.
       */
      continue;
     }
     sp->pcount[ridx]++;
 } else {
     sp->pcount[ridx]++;
     sp->addr[ridx] = malloc(packet->rlen);
     if (sp->addr[ridx] == NULL) {
  sp->pcount[3]++;
  rtpp_log_write(RTPP_LOG_ERR, sp->log,
    "can't allocate memory for remote address - "
    "removing session");
  remove_session(cf, GET_RTP(sp));
  /* Break, sp is invalid now */
  break;
     }
     /* Signal that an address have to be updated. */
     rtpp_log_write(RTPP_LOG_ERR, cf->glog, "IP Handover Set i 2nd ridx %d",ridx); 
     i = 1;
 }

/*
  * Update recorded address if it's necessary. Set "untrusted address"
  * flag in the session state, so that possible future address updates
  * from that client won't get address changed immediately to some
  * bogus one.
  */
 if (i != 0) {
     sp->untrusted_addr[ridx] = 1;
     memcpy(sp->addr[ridx], &packet->raddr, packet->rlen);

     // IP Handover Do not use canupdate
     // After update, reset src_count
     /*
     if (sp->prev_addr[ridx] == NULL || memcmp(sp->prev_addr[ridx],
       &packet->raddr, packet->rlen) != 0) 
     {
         sp->canupdate[ridx] = 0;
  if(sp->prev_addr[ridx] == NULL)
  {
     rtpp_log_write(RTPP_LOG_ERR, cf->glog, "IP Handover prev_addr NULL ridx %d",ridx); 
  }
  rtpp_log_write(RTPP_LOG_ERR, cf->glog, "IP Handover Set canupdate to 0 2nd ridx %d",ridx);
     }
     */

sp->src_count[ridx] = 0;

port = ntohs(satosin(&packet->raddr)->sin_port);

rtpp_log_write(RTPP_LOG_INFO, sp->log,
       "%s's address filled in: %s:%d (%s)",
       (ridx == 0) ? "callee" : "caller",
       addr2char(sstosa(&packet->raddr)), port,
       (sp->rtp == NULL) ? "RTP" : "RTCP");

/*
      * Check if we have updated RTP while RTCP is still
      * empty or contains address that differs from one we
      * used when updating RTP. Try to guess RTCP if so,
      * should be handy for non-NAT'ed clients, and some
      * NATed as well.
      */
     if (sp->rtcp != NULL && (sp->rtcp->addr[ridx] == NULL ||
       !ishostseq(sp->rtcp->addr[ridx], sstosa(&packet->raddr)))) {
  if (sp->rtcp->addr[ridx] == NULL) {
      sp->rtcp->addr[ridx] = malloc(packet->rlen);
      if (sp->rtcp->addr[ridx] == NULL) {
   sp->pcount[3]++;
   rtpp_log_write(RTPP_LOG_ERR, sp->log,
     "can't allocate memory for remote address - "
     "removing session");
   remove_session(cf, sp);
   /* Break, sp is invalid now */
   break;
      }
  }
  memcpy(sp->rtcp->addr[ridx], &packet->raddr, packet->rlen);
  satosin(sp->rtcp->addr[ridx])->sin_port = htons(port + 1);
  /* Use guessed value as the only true one for asymmetric clients */
  sp->rtcp->canupdate[ridx] = NOT(sp->rtcp->asymmetric[ridx]);
  rtpp_log_write(RTPP_LOG_INFO, sp->log, "guessing RTCP port "
    "for %s to be %d",
    (ridx == 0) ? "callee" : "caller", port + 1);
     }
 }

if (sp->resizers[ridx].output_nsamples > 0)
     rtp_resizer_enqueue(&sp->resizers[ridx], &packet);
 if (packet != NULL)
     send_packet(cf, sp, ridx, packet);
    }

if (packet != NULL)
 rtp_packet_free(packet);
}

Here are some useful resources that I read