How to support right click menu to NSStatusItem

Issue #707

The trick is to set the button oinside of statusItem to send actions on both leftMouseUp and rightMouseUp.

Another thing to note is we use popUpMenu on NSStatusItem, although it is marked as deprecated on macOS 10.14. We can set menu but that overrides left click.

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

private let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
private let statusItemMenuHandler = MenuHandler()

func setupStatusMenu() {
if let button = statusItem.button {
button.image = NSImage(named: NSImage.Name("statusMenuIcon"))
button.contentTintColor = NSColor.black
button.action = #selector(statusMenuButtonTouched(_:))
button.sendAction(on: [.leftMouseUp, .rightMouseUp]) // This is important

statusItemMenuHandler.add(title: "About", action: {
NSWorkspace.shared.open(URL(string: "https://onmyway133.com/pushhero")!)
})
}
}

@objc
private func statusMenuButtonTouched(_ sender: NSStatusBarButton) {
guard let event = NSApp.currentEvent else { return }
switch event.type {
case .rightMouseUp:
statusItem.popUpMenu(statusItemMenuHandler.menu)
// statusItem.menu = statusItemMenuHandler.menu // this overrides left click
default:
popover.toggle()
}
}

Updated at 2020-12-08 05:11:24

How to convert struct to Core Data NSManagedObject

Issue #706

Use Mirror and set key value as NSManagedObject subclasses from NSObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import CoreData

final class ManagedObjectConverter {
func convert<M>(m: M, context: NSManagedObjectContext) throws -> NSManagedObject {
let entityName = String(describing: m)
guard let entityDescription = NSEntityDescription.entity(
forEntityName: entityName,
in: context
) else {
throw AppError.parsing
}

let managedObject = NSManagedObject(
entity: entityDescription,
insertInto: context
)

let mirror = Mirror(reflecting: m)

guard mirror.displayStyle == .struct else {
throw AppError.parsing
}

for case let (label?, anyValue) in mirror.children {
managedObject.setValue(anyValue, forKey: label)
}

return managedObject
}
}

Updated at 2020-12-07 06:05:04

How to declare Error in Swift

Issue #704

We used to declare enum that conforms to Error, but any type like struct or class can conform to Error as well.

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
enum NetworkError: Error {
case failToCreateRequest
case failToParseResponse
case failToReachServe
}

struct DetailError: Error {
let networkError: Error
let createdAt: Date
let tag: String
}

final class TrackError: Error {
let trackId: String
let detailError: DetailError
let trackSession: String

init(trackId: String, detailError: DetailError, trackSession: String) {
self.trackId = trackId
self.detailError = detailError
self.trackSession = trackSession
}
}

let networkError = NetworkError.failToCreateRequest
let detailError = DetailError(networkError: networkError, createdAt: Date(), tag: "0.1")
let trackError = TrackError(trackId: "AB-01", detailError: detailError, trackSession: "101")

How to convert from paid to free with IAP

Issue #703

What is receipt

Read When to refresh a receipt vs restore purchases in iOS?

From iOS 7, every app downloaded from the store has a receipt (for downloading/buying the app) at appStoreReceiptURL. When users purchases something via In App Purchase, the content at appStoreReceiptURL is updated with purchases information. Most of the cases, you just need to refresh the receipt (at appStoreReceiptURL) so that you know which transactions users have made.

Note

  • Receipt is generated and bundled with your app when user download the app, whether it is free or paid
  • When user makes IAP, receipt is updated with IAP information
  • When user downloads an app (download free, or purchase paid app), they get future updates (whether free or paid) forever.
  • Call SKReceiptRefreshRequest or SKPaymentQueue.restoreCompletedTransactions asks for Appstore credential
  • When we build the app from Xcode or download from Testflight, receipt is not bundled within the app since the app is not downloaded from AppStore. We can use SKReceiptRefreshRequest to download receipt from sandbox Appstore
  • restoreCompletedTransactions updates app receipt
  • Receipt is stored locally on device, so when user uninstalls and reinstalls your app, there’s no in app purchases information, this is when you should refresh receipt or restoreCompletedTransactions

Users restore transactions to maintain access to content they’ve already purchased. For example, when they upgrade to a new phone, they don’t lose all of the items they purchased on the old phone. Include some mechanism in your app to let the user restore their purchases, such as a Restore Purchases button. Restoring purchases prompts for the user’s App Store credentials, which interrupts the flow of your app: because of this, don’t automatically restore purchases, especially not every time your app is launched.

In most cases, all your app needs to do is refresh its receipt and deliver the products in its receipt. The refreshed receipt contains a record of the user’s purchases in this app, on this device or any other device. However, some apps need to take an alternate approach for one of the following reasons:

  • If you use Apple-hosted content, restoring completed transactions gives your app the transaction objects it uses to download the content. If you need to support versions of iOS earlier than iOS 7, where the app receipt isn’t available, restore completed transactions instead.
  • Refreshing the receipt asks the App Store for the latest copy of the receipt. Refreshing a receipt does not create any new transactions.
  • Restoring completed transactions creates a new transaction for every completed transaction the user made, essentially replaying history for your transaction queue observer.

More about receipt, from WWDC 2017, What’s new in StoreKit session https://developer.apple.com/videos/play/wwdc2017/303/

receipt

You can also watch WWDC 2017, session Advanced StoreKit for more detail https://developer.apple.com/videos/play/wwdc2017/305/

receipt tips

Restoring Purchased Products

Read Restoring Purchased Products

Users sometimes need to restore purchased content, such as when they upgrade to a new phone.

Don’t automatically restore purchases, especially when your app is launched. Restoring purchases prompts for the user’s App Store credentials, which interrupts the flow of your app

In most cases, you only need to refresh the app receipt and deliver the products listed on the receipt. The refreshed receipt contains a record of the user’s purchases in this app, from any device the user’s App Store account is logged into

Refreshing a receipt doesn’t create new transactions; it requests the latest copy of the receipt from the App Store

Restoring completed transactions creates a new transaction for every transaction previously completed, essentially replaying history for your transaction queue observer. Your app maintains its own state to keep track of why it’s restoring completed transactions and how to handle them.

What are the different IAP types

From AppStore

Consumable (pay everytime)

A consumable In-App Purchase must be purchased every time the user downloads it. One-time services, such as fish food in a fishing app, are usually implemented as consumables.

Non-Consumable (one time payment)

Non-consumable In-App Purchases only need to be purchased once by users. Services that do not expire or decrease with use are usually implemented as non-consumables, such as new race tracks for a game app.

Auto-Renewable Subscriptions (will deduct money from your credit card on a cycle complete)

Auto-renewable Subscriptions allow the user to purchase updating and dynamic content for a set duration of time. Subscriptions renew automatically unless the user opts out, such as magazine subscriptions.

Free Subscription (no payment and is still visible even you did not submitted your account detail to itunes connect)

Free subscriptions are a way for developers to put free subscription content in Newsstand. Once a user signs up for a free subscription, it will be available on all devices associated with the user’s Apple ID. Note that free subscriptions do not expire and can only be offered in Newsstand-enabled apps.

Non-Renewing (need to renew manually)

Subscription Non-Renewing Subscriptions allow the sale of services with a limited duration. Non-Renewing Subscriptions must be used for In-App Purchases that offer time-based access to static content. Examples include a one week subscription to voice guidance feature within a navigation app or an annual subscription to online catalog of archived video or audio.

When is app receipt missing

Read SKReceiptRefreshRequest

Use this API to request a new receipt if the receipt is invalid or missing

Receipt is stored locally on device. It can be missing in case user sync or restore device.

Watch WWDC 2014 - 305 Preventing Unauthorized Purchases with Receipts

How to check receipt existence

1
2
Bundle.main.appStoreReceiptURL
checkResourceIsReachable

How to read receipt

Read In-App Purchases: Receipt Validation Tutorial

The receipt consists of a single file in the app bundle. The file is in a format called PKCS #7. The payload consists of a set of receipt attributes in a cross-platform format called ASN.1

1
2
3
4
5
6
7
8
9
10
case 12: // Receipt Creation Date
var dateStartPtr = ptr
receiptCreationDate = readASN1Date(ptr: &dateStartPtr, maxLength: length)

case 17: // IAP Receipt
print("IAP Receipt.")

case 19: // Original App Version
var stringStartPtr = ptr
originalAppVersion = readASN1String(ptr: &stringStartPtr, maxLength: length)

Use TPInAppReceipt which includes certificates.

1
try InAppReceipt.localReceipt()

Check Receipt Fields

1
2
3
4
Original Application Version
The version of the app that was originally purchased.
ASN.1 Field Type 19
ASN.1 Field Value UTF8STRING

Note

This corresponds to the value of CFBundleVersion (in iOS) or CFBundleShortVersionString (in macOS) in the Info.plist file when the purchase was originally made

CFBundleVersion is build number, and CFBundleShortVersionString is app version

1
2
3
In-App Purchase Receipt
The receipt for an in-app purchase.
ASN.1 Field Type 17

Read Validating Receipts with the App Store

Sample verifyReceipt json

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
{
"receipt": {
"receipt_type": "ProductionSandbox",
"adam_id": 0,
"app_item_id": 0,
"bundle_id": "com.example.app.ios",
"application_version": "3",
"download_id": 0,
"version_external_identifier": 0,
"receipt_creation_date": "2018-11-13 16:46:31 Etc/GMT",
"receipt_creation_date_ms": "1542127591000",
"receipt_creation_date_pst": "2018-11-13 08:46:31 America/Los_Angeles",
"request_date": "2018-11-13 17:10:31 Etc/GMT",
"request_date_ms": "1542129031280",
"request_date_pst": "2018-11-13 09:10:31 America/Los_Angeles",
"original_purchase_date": "2013-08-01 07:00:00 Etc/GMT",
"original_purchase_date_ms": "1375340400000",
"original_purchase_date_pst": "2013-08-01 00:00:00 America/Los_Angeles",
"original_application_version": "1.0",
"in_app": [{
"quantity": "1",
"product_id": "test2",
"transaction_id": "1000000472106082",
"original_transaction_id": "1000000472106082",
"purchase_date": "2018-11-13 16:46:31 Etc/GMT",
"purchase_date_ms": "1542127591000",
"purchase_date_pst": "2018-11-13 08:46:31 America/Los_Angeles",
"original_purchase_date": "2018-11-13 16:46:31 Etc/GMT",
"original_purchase_date_ms": "1542127591000",
"original_purchase_date_pst": "2018-11-13 08:46:31 America/Los_Angeles",
"is_trial_period": "false"
}]
},
"status": 0,
"environment": "Sandbox"
}

Verify your receipt first with the production URL; then verify with the sandbox URL if you receive a 21007 status code. This approach ensures you do not have to switch between URLs while your application is tested, reviewed by App Review, or live in the App Store.

Show me the code

Let’s use enum to represent possible states for each resource. Here’s simple case where we only have 1 non consumable IAP product.

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
enum IAPError: Error {
case failedToRefreshReceipt
case failedToRequestProduct
case failedToPurchase
case receiptNotFound
}

enum IAPResourceState<T> {
case notAsked
case loading
case success(T)
case failure(IAPError)
}

final class PricingPlan: ObservableObject {
static let pro = (Bundle.main.bundleIdentifier ?? "") + ".pro"

@Published
var isPro: Bool = false
@Published
var product: IAPResourceState<SKProduct> = .notAsked
@Published
var purchase: IAPResourceState<SKPayment> = .notAsked
@Published
var receipt: IAPResourceState<InAppReceipt> = .notAsked
}

Let’s have a central place for managing all IAP operations, called IAPManager, it can update our ObservableObject PricingPlan hence triggers update to SwiftUI.

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
import StoreKit
import TPInAppReceipt
import Version

final class IAPManager: NSObject {
private let pricingPlan: PricingPlan
private let paymentQueue: SKPaymentQueue

init(pricingPlan: PricingPlan) {
self.pricingPlan = pricingPlan
self.paymentQueue = SKPaymentQueue.default()

super.init()

self.paymentQueue.add(self)
}

func requestProducts() {
let identifiers = PricingPlan.pro
let request = SKProductsRequest(productIdentifiers: Set(arrayLiteral: identifiers))
request.delegate = self
pricingPlan.product = .loading
request.start()
}

func purchase(product: SKProduct) {
guard SKPaymentQueue.canMakePayments() else {
showAlert(text: "You are not allowed to make payment. Please check device settings.")
return
}

pricingPlan.purchase = .loading
let payment = SKPayment(product: product)
paymentQueue.add(payment)
}

func refreshReceipt() {
let request = SKReceiptRefreshRequest()
request.delegate = self
request.start()
}

func restorePurchase() {
paymentQueue.restoreCompletedTransactions()
}
}

Refresh receipt

You can use restoreCompletedTransactions if you simply finishTransaction and grant user pro feature, like in this simple tutorial In-App Purchase Tutorial: Getting Started, search for SKPaymentTransactionObserver. restoreCompletedTransactions also updates receipt.

Otherwise refreshing receipt is a better idea. It serves both case when receipt is not there locally and when you want to restore transactions. With receipt refreshing, no restored transactions are created and SKPaymentTransactionObserver is not called, so we need to check receipt proactively.

Either restoreCompletedTransactions or SKReceiptRefreshRequest asks for AppStore credential so you should present a button there and ask user.

Check local receipt

Try to locate local receipt and examine it.

  • If it is not there (missing, corrupted), refresh receipt
  • If it’s there, check if it was from a version when the app was still as paid. Notice the difference in meaning of originalAppVersion in macOS and iOS
  • If it is not paid, check if this receipt contains In App Purchase information for our product

In practice, we need to perform some basic checks on receipt, like bundle id, app version, device id. Read In-App Purchases: Receipt Validation Tutorial, search for Validating the Receipt. TPInAppReceipt also has some handy verify functions

Besides verifying receipt locally, it is advisable to call verifyreceipt either on device, or better on serve to let Apple verify receipt and returns you a human readable json for receipt information.

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
func checkReceipt() {
DispatchQueue.main.async {
do {
let receipt = try InAppReceipt.localReceipt()
self.pricingPlan.receipt = .success(receipt)
if self.isPaid(receipt: receipt) {
self.pricingPlan.isPro = true
} else if receipt.containsPurchase(ofProductIdentifier: PricingPlan.pro) {
self.pricingPlan.isPro = true
}
} catch {
self.pricingPlan.receipt = .failure(.receiptNotFound)
}
}
}

private func isPaid(receipt: InAppReceipt) -> Bool {
#if os(macOS)
// originalAppVersion is CFBundleShortVersionString
if let version = Version(receipt.originalAppVersion) {
return version < versionToIAP
}
#else
// originalAppVersion is CFBundleVersion
if let buildNumber = Int(receipt.originalAppVersion) {
return buildNumber < buildNumberToIAP
}
#endif
return false
}

Finally, observe SKProductsRequestDelegate which also conforms to SKRequestDelegate for both product and receipt refresh request

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
extension IAPManager: SKProductsRequestDelegate {
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
DispatchQueue.main.async {
guard let product = response.products.first else {
self.pricingPlan.product = .failure(IAPError.failedToRequestProduct)
return
}

self.pricingPlan.product = .success(product)
}
}

func request(_ request: SKRequest, didFailWithError error: Error) {
DispatchQueue.main.async {
switch request {
case is SKProductsRequest:
self.pricingPlan.product = .failure(IAPError.failedToRequestProduct)
case is SKReceiptRefreshRequest:
self.pricingPlan.receipt = .failure(IAPError.failedToRefreshReceipt)
default:
break
}

}
}

func requestDidFinish(_ request: SKRequest) {
switch request {
case is SKReceiptRefreshRequest:
checkReceipt()
default:
break
}
}
}

Updated at 2020-12-04 06:58:17

How to search using regular expression in Xcode

Issue #698

Xcode has powerful search. We can constrain search to be scoped in workspace, project or some folders. We can also constrain case sensitivity.

Another cool thing that people tend to overlook is, besides searching based on text, we can search based on references, definitions, call hierarchy, and 🎉 regular expressions.

Screenshot 2020-12-11 at 11 27 48

Searching for regular expression gives us extra power when it comes to limit our search based on some criteria. For example when we are about to refactor some NSLayoutAnchor code and we want to find all NSLayoutConstraint. calls that stops at bottomAnchor

Here ‘s how to search NSLayoutConstraint calls that involves bottomAnchor

1
(NSLayoutConstraint(.*[\r\n])*).+?(?=bottomAnchor)
1
2
3
4
5
6
NSLayoutConstraint.activate([
child.leadingAnchor.constraint(equalTo: parent.safeAreaLayoutGuide.leadingAnchor),
child.trailingAnchor.constraint(equalTo: parent.safeAreaLayoutGuide.trailingAnchor),
child.topAnchor.constraint(equalTo: parent.safeAreaLayoutGuide.topAnchor),
child.bottomAnchor.constraint(equalTo: parent.safeAreaLayoutGuide.bottomAnchor)
])

Another tip when searching for regular expression is that we can use https://regex101.com/ to validate and fine tune our regex. Below are breakdowns of our regex. Note how we use /.+?(?=abc)/ to define “search until”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/
(NSLayoutConstraint(.*[\r\n])*).+?(?=bottomAnchor)
/
gm
1st Capturing Group (NSLayoutConstraint(.*[\r\n])*)
NSLayoutConstraint matches the characters NSLayoutConstraint literally (case sensitive)
2nd Capturing Group (.*[\r\n])*
* Quantifier — Matches between zero and unlimited times, as many times as possible, giving back as needed (greedy)
A repeated capturing group will only capture the last iteration. Put a capturing group around the repeated group to capture all iterations or use a non-capturing group instead if you're not interested in the data
.* matches any character (except for line terminators)
* Quantifier — Matches between zero and unlimited times, as many times as possible, giving back as needed (greedy)
Match a single character present in the list below [\r\n]
\r matches a carriage return (ASCII 13)
\n matches a line-feed (newline) character (ASCII 10)
.+? matches any character (except for line terminators)
+? Quantifier — Matches between one and unlimited times, as few times as possible, expanding as needed (lazy)
Positive Lookahead (?=bottomAnchor)
Assert that the Regex below matches
bottomAnchor matches the characters bottomAnchor literally (case sensitive)
Global pattern flags
g modifier: global. All matches (don't return after first match)
m modifier: multi line. Causes ^ and $ to match the begin/end of each line (not only begin/end of string)

Updated at 2020-12-11 10:35:14

How to write to temporary file in Swift

Issue #697

Use temporaryDirectory from FileManager and String.write

1
2
3
4
5
6
7
8
9
10
11
12
func writeTempFile(books: [Book]) -> URL {
let url = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension("txt")
let string = books
.map({
"book '\($0.url.path)'"
})
.joined(separator: "\n")
try? string.write(to: url, atomically: true, encoding: .utf8)
return url
}

How to use functions with default arguments in Swift

Issue #696

Which methods do you think are used here

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import Cocoa

struct Robot {
let a: Int
let b: Int
let c: Int

init(a: Int = 1, c: Int = 3) {
self.a = a
self.b = 0
self.c = c
print("Init with a=\(a) and c=\(c)")
}

init(a: Int = 1, b: Int = 2, c: Int = 3) {
self.a = a
self.b = b
self.c = c

print("Init with a\(a), b=\(b) and c=\(c)")
}
}

let r1 = Robot(c: 10)
let r2 = Robot(a: 5, c: 10)
let r3 = Robot(a: 5, b: 7, c: 10)
let r4 = Robot(a: 5)
let r5 = Robot(b: 5)

The log is

1
2
3
4
5
Init with a=1 and c=10
Init with a=5 and c=10
Init with a5, b=7 and c=10
Init with a=5 and c=3
Init with a1, b=5 and c=3

How to check IAP Transaction error

Issue #695

Inspect SKPaymentTransaction for error. In Swift, any Error can be safely bridged into NSError there you can check errorDomain and code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private func handleFailure(_ transaction: SKPaymentTransaction) {
guard let error = transaction.error else { return }
let nsError = error as NSError
guard nsError.domain == SKError.errorDomain else { return }

switch nsError.code {
case SKError.clientInvalid.rawValue, SKError.paymentNotAllowed.rawValue:
showAlert(text: "You are not allowed to make payment.")
case SKError.paymentCancelled.rawValue:
showAlert(text: "Payment has been cancelled.")
case SKError.unknown.rawValue, SKError.paymentInvalid.rawValue:
fallthrough
default:
showAlert(text: "Something went wrong making payment.")
}
}

How to use nested ObservableObject in SwiftUI

Issue #694

I usually structure my app to have 1 main ObservableObject called Store with multiple properties in it.

1
2
3
4
5
6
7
8
9
10
11
12
final class Store: ObservableObject {
@Published var pricingPlan: PricingPlan()
@Published var preferences: Preferences()
}

struct Preferences {
var opensAtLogin: Bool = true
}

final class PricingPlan: ObservableObject {
@Published var isPro: Bool = true
}

SwiftUI for now does not work with nested ObservableObject, so if I pass Store to PricingView, changes in PricingPlan does not trigger view update in PricingView.

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


var body: some View {
VStack { geo in

}
.sheet(isPresented: $showsPricing) {
PricingView(
isPresented: $showsPricing,
store: store
)
}
}
}

There are some workarounds

Pass nested ObservableObject

So that View observes both parent and nested objects.

1
2
3
4
PricingView(
store: store,
pricingPlan: store.pricingPlan
)

Use struct

This forces us to deal with immutability also, as with reference type PricingPlan, someone could just save a reference to it and alter it at some point.

1
struct PricingPlan {}

Listen to nested objects changes

Every ObservableObject has a synthesized property objectWillChange that triggers when any @Publisshed property changes

1
2
3
4
5
6
7
8
9
10
final class Store: ObservableObject {
@Published var pricingPlan = PricingPlan()

private var anyCancellable: AnyCancellable? = nil

init() {
anyCancellable = pricingPlan.objectWillChange.sink { [weak self] _ in
self?.objectWillChange.send()
}
}

How to check dark mode in AppKit for macOS apps

Issue #693

AppKit app has its theme information stored in UserDefaults key AppleInterfaceStyle, if is dark, it contains String Dark.

Another way is to detect appearance via NSView

1
2
3
4
5
6
7
8
9
struct R {
static let dark = DarkTheme()
static let light = LightTheme()

static var theme: Theme {
let isDark = UserDefaults.standard.string(forKey: "AppleInterfaceStyle") == "Dark"
return isDark ? dark : light
}
}

Another way is to rely on appearance on NSView. You can quickly check via NSApp.keyWindow?.effectiveAppearance but notice that keyWindow can be nil when the app is not active since no window is focused for keyboard events. You should use NSApp.windows.first

1
let isDark = NSApp.windows.first?.effectiveAppearance.bestMatch(from: [.darkAqua, .vibrantDark]) == .darkAqua

Then build a simple Theme system

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

protocol Theme {
var primaryColor: Color { get }
var textColor: Color { get }
var text2Color: Color { get }
var backgroundColor: Color { get }
var background2Color: Color { get }
}

extension Theme {
var primaryColor: Color { Color(hex: 0x1) }
}

struct DarkTheme: Theme {
...
}

struct LightTheme: Theme {
....
}

For SwiftUI, you can colorScheme environment, then use modifier .id(colorScheme) to force SwiftUI to update when color scheme changes

1
2
3
4
5
6
7
8
9
struct MainView: View {
@Environment(\.colorScheme)
var colorScheme

var body: some View {
VStack {}
.id(colorScheme)
}
}

Updated at 2020-11-10 05:52:33

How to avoid multiple match elements in UITests from iOS 13

Issue #691

Supposed we want to present a ViewController, and there exist both UIToolbar in both the presenting and presented view controllers.

From iOS 13, the model style is not full screen and interactive. From UITests perspective there are 2 UIToolbar, we need to specify the correct one to avoid multiple match errors

1
let editButton = app.toolbars["EditArticle.Toolbar"].buttons["Edit"]

Updated at 2020-11-04 10:02:29

How to use accessibility container in UITests

Issue #690

Use accessibilityElements to specify containment for contentView and buttons. You can use Accessibility Inspector from Xcode to verify.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ArticleCell: UICollectionViewCell {
let authorLabel: UILabel
let dateLabel: UILabel

let viewLabel: UILabel
let deleteButton: UIButton

private func setupAccessibility() {
contentView.isAccessibilityElement = true
contentView.accessibilityLabel = "This article is written by Nobita on Dec 4th 2020"

viewLabel.isAccessibilityElement = true // Default is false
viewLabel.accessibilityTraits.insert(.button) // Treat UILabel as button to VoiceOver

accessibilityElements = [contentView, viewLabel, deleteButton]
isAccessibilityElement = false
}
}

This works OK under Voice Over and Accessibility Inspector. However in iOS 14 UITests, only the contentView is recognizable. The workaround is to use XCUICoordinate

1
2
3
4
5
6
7
8
9
let deleteButton = cell.buttons["Delete article"]
if deleteButton.waitForExistence(timeout: 1) {
deleteButton.tap()
} else {
let coordinate = cell.coordinate(
withNormalizedOffset: CGVector(dx: 0.9, dy: 0.9)
)
coordinate.tap()
}

Updated at 2020-11-04 09:42:05

How to make full size content view in SwiftUI for macOS

Issue #689

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
func applicationDidFinishLaunching(_ aNotification: Notification) {
// extend to title bar
let contentView = ContentView()
// .padding(.top, 24) // can padding to give some space
.edgesIgnoringSafeArea(.top)

// specify fullSizeContentView
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 800, height: 600),
styleMask: [.titled, .closable, .miniaturizable, .texturedBackground, .resizable, .fullSizeContentView],
backing: .buffered,
defer: false
)
window.center()
window.setFrameAutosaveName("My App")

// window.title = ... // no title
// window.toolbar = NSToolbar() // use toolbar if wanted. This triggers .unifiedTitleAndToolbar styleMask
window.titlebarAppearsTransparent = true
window.titleVisibility = .hidden // hide title
window.backgroundColor = R.color.myBackgroundColor // set our preferred background color

window.contentView = NSHostingView(rootView: contentView)
window.makeKeyAndOrderFront(nil)
}

I have an issue where if I use HSplitView, some Button are not clickable until I drag the split view handle. The workaround is to use HStack with a Divider

Updated at 2020-11-03 05:21:50

How to override styles in SwiftUI

Issue #688

In the app I’m working on Elegant Converter, I usually like preset theme with a custom background color and a matching foreground color.

Thanks to SwiftUI style cascading, I can just declare in root MainView and it will be inherited down the view hierachy.

1
2
3
4
5
6
7
8
9
10
struct MainView: View {
var body: some View {
HSplitView {
ListView()
RightView()
}
.foregroundColor(R.color.text)
.background(R.color.background)
}
}

This works great regardless of system light or dark mode, but in light mode it does not look good, as my designed theme is similar to dark mode. Here the color is pale for default Picker and Button.

Screenshot 2020-10-31 at 06 49 55

The way to fix that is to reset the foregroundColor, take a look at the documentation for foregroundColor

The foreground color to use when displaying this view. Pass nil to remove any custom foreground color and to allow the system or the container to provide its own foreground color. If a container-specific override doesn’t exist, the system uses the primary color.

So we can pass nil and the controls will pick the correct color based on system preferences.

1
2
3
4
5
6
7
8
9
10
11
Picker(selection: intent.fileType, label: EmptyView()) {
ForEach(filtered, id: \.self) {
Text($0.dropdownName)
}
}
.foregroundColor(nil)

Button(action: convert) {
Text("Convert")
}
.foregroundColor(nil)

Now Picker and Button look nice in both dark and light mode

Screenshot 2020-10-31 at 06 52 53 Screenshot 2020-10-31 at 06 53 10

Updated at 2020-11-01 04:57:11

When to use function vs property in Swift

Issue #687

Although I do Swift, I often follow Kotlin guideline https://kotlinlang.org/docs/reference/coding-conventions.html#functions-vs-properties

In some cases functions with no arguments might be interchangeable with read-only properties. Although the semantics are similar, there are some stylistic conventions on when to prefer one to another.

Prefer a property over a function when the underlying algorithm:

  • does not throw
  • is cheap to calculate (or cached on the first run)
  • returns the same result over invocations if the object state hasn’t changed

Updated at 2020-10-27 09:56:38

How to use CoreData safely

Issue #686

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
extension SendHistoryItem {
func toCoreData(context: NSManagedObjectContext) {
context.perform {
let cd = CDSendHistoryItem(context: context)
}
}
}

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

}
}

final class CoreDataManager {
private var backgroundContext: NSManagedObjectContext?

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

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

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

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

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

Read more


Updated at 2020-10-25 20:58:07

How to pass ObservedObject as parameter in SwiftUI

Issue #685

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

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

@State
private var selectedTask: AnyTask

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

struct ParentView: View {
@ObservedObject
var store: Store

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

How to do equal width in SwiftUI

Issue #684

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

The same applies in vertical direct also.

width


Updated at 2020-10-23 08:17:08

How to test push notifications in simulator and production iOS apps

Issue #682

After has recently reminded about his updating APNs provider API which makes me realised a lot has changed about push notifications, both in terms of client and provider approach.

The HTTP/2-based Apple Push Notification service (APNs) provider API lets you take advantage of great features, such as authentication with a JSON Web Token, improved error messaging, and per-notification feedback. If you send push notifications with the legacy binary protocol, we strongly recommend upgrading to the APNs provider API.

From the the iOS client point of view, you pretty much don’t need to care about provider API, as you only need to register for push notification capability and send the device token to your backend, and it’s the backend’s job to send push request to Apple notifications server.

But it’s good to know the underlying mechanism, especially when you want to troubleshoot push notification. Since a push notification failure can be because of many reasons: it could be due to some failed logic in your backend, or that the push certificate has expired, or that you have sent the wrong device token, or that user has turned off push permission in Settings app, or that the push has been delayed.

Here’s also a common abbreviations you need to learn: APNs it is short for Apple Push Notification service. You can think of it as the server your device always has connection with, so you’re ready for any server push messages from it.

What is binary provider API

Push notification feature was first introduced by Apple in 2009 via a fairly complex binary provider API

binary

In short, binary provider API is just a specification about which address and which format to send push request to Apple push server. The binary interface of the production environment is available through gateway.push.apple.com, port 2195 and development environment gateway.sandbox.push.apple.com, port 2195. The binary interface employs a plain TCP socket for binary content that is streaming in nature.

As you can see in the package instruction above, we need specify frame length and data. Data is a key value pairs of multiple informations like device token, expiration, topic, priority.

Send request to the above addresses via secured TLS or SSL channel. The SSL certificate required for these connections is obtained from your developer account.

The new HTTP/2 provider API

The new HTTP2/ provider API is recommended, it has detailed error reporting and better throughput. If you use URLSession which supports the HTTP/1.1 and HTTP/2 protocols. HTTP/2 support, as described by RFC 7540, you will feel familiar.

In short, when you have a notification to send to a user, your provider must construct a POST request and send it to Apple Push Notification service (APNs). Your request must include the following information:

  • The JSON payload that you want to send
  • The device token for the user’s device
  • Request-header fields specifying how to deliver the notification
  • For token-based authentication, your provider server’s current authentication token

Upon receiving your server’s POST request, APNs validates the request using either the provided authentication token or your server’s certificate. If validation succeeds, APNs uses the provided device token to identify the user’s device. It then tries to send your JSON payload to that device.

So it’s pretty much how you configure URLRequestwith URLSession, specify base url, some request headers including the authorization and the payload body.

Use HTTP/2 and TLS 1.2 or later to establish a connection to the new provider API endpoint. For development serverit is api.sandbox.push.apple.com:443 and for production server it is api.push.apple.com:443. You then send the request as POST and Apple will do its job to verify and send the push notification to the users device.

Certificate vs token based authentication

To send push request to APNs, you need to authenticate to tell that is really from you. APNs support 2 types of authentication, the traditional way with a certificate, and the recently new recommended way with a p8 token.

Certificate based authentication

For certificate based authentication, you will need a p12 certificate which you can obtain and generate from your Developer account.

cer

Because there are sandbox and production push endpoint, few years ago it was required to have separate sandbox and production environment push certificate, which you had to create separately in your Apple developer account, then in testing need to specify the correct push certificate for each environment.

Now around iOS 10 year we can use just 1 single certificate for both sandbox and production, which is a relief for us developers. When we create certificate on Apple developer page, we need to upload a certificate signing request that we can generate from Keychain Access app. After we download the generated push certificate, download and run it in Keychain, there we can generate p12 key file that contains both certificate and private key to sign our push request.

Certificate and provisioning profiles valid for only 1 year. So every year you have to renew push notification certificates, which is also a good and bad thing. Besides that, every app differs from bundle id, so we kinda have to generate certificate for each app.

Token based authentication

Token-based authentication offers a stateless way to communicate with APNs. Stateless communication is faster than certificate-based communication because it doesn’t require APNs to look up the certificate, or other information, related to your provider server. There are other advantages to using token-based authentication:

The cool thing about token is that you can use one token to distribute notifications for all of your company’s apps. You can create in your Apple developer account. Key authentication is the recommended way to authenticate with Apple services, so it is used not only for push services, but also for MusicKit and DeviceCheck.

key

When you request a key, Apple gives you a 10-character string with the Key ID which you must include this string in your JSON tokens and an authentication token signing key, specified as a text file (with a .p8 file extension). The key is allowed to download once and you need to keep it properly.

The token that you include with your notification requests uses the JSON Web Token (JWT) specification. The token itself contains four key-value pairs

Screenshot 2020-10-11 at 06 28 34

After all, the JSON Web Token is encoded in this authorization HTTP headers in your request like this

1
2
3
authorization = bearer eyAia2lkIjogIjhZTDNHM1JSWDciIH0.eyAiaXNzIjogIkM4Nk5WOUpYM0QiLCAiaWF0I
jogIjE0NTkxNDM1ODA2NTAiIH0.MEYCIQDzqyahmH1rz1s-LFNkylXEa2lZ_aOCX4daxxTZkVEGzwIhALvkClnx5m5eAT6
Lxw7LZtEQcH6JENhJTMArwLf3sXwi

For security, APNs requires you to refresh your token regularly. Refresh your token no more than once every 20 minutes and no less than once every 60 minutes.

How to register for push notifications from iOS app

The APIs to register for remote notification has changed over the years.

iOS 7

In iOS 7, we used to use this method registerForRemoteNotificationTypes to register to receive remote notifications of the specified types via Apple Push Notification service.

The types can be UIRemoteNotificationTypeBadge, UIRemoteNotificationTypeAlert, UIRemoteNotificationTypeSound

When you send this message, the device initiates the registration process with Apple Push Notification service. If it succeeds, the app delegate receives a device token in the application:didRegisterForRemoteNotificationsWithDeviceToken: method; if registration doesn’t succeed, the delegate is informed via the application:didFailToRegisterForRemoteNotificationsWithError:method. If the app delegate receives a device token, it should connect with its provider and pass it the token.

iOS 8 with registerUserNotificationSettings

From iOS 8, there’s separation between asking for a remote notification with device token, and with presenting push message to the user. This confused developers as these 2 things are separate now.

First, we use registerForRemoteNotifications to register to receive remote notifications via Apple Push Notification service.

Call this method to initiate the registration process with Apple Push Notification service. If registration succeeds, the app calls your app delegate object’s application(_:didRegisterForRemoteNotificationsWithDeviceToken:) method and passes it a device token. You should pass this token along to the server you use to generate remote notifications for the device. If registration fails, the app calls its app delegate’s application(_:didFailToRegisterForRemoteNotificationsWithError:) method instead.

In short, this is to receive device token from APNs so we can do silent push notification or other things. Note that we need to enable Remote notification capability for Background modes.

Screenshot 2020-10-11 at 06 39 46

To present push message to user via alert, banner, badge or sound, we need to explicitly ask for using this method registerUserNotificationSettings to registers your preferred options for notifying the user.

If your app displays alerts, play sounds, or badges its icon, you must call this method during your launch cycle to request permission to alert the user in these ways. After calling this method, the app calls the application(_ application: UIApplication, didRegister notificationSettings: UIUserNotificationSettings) method of its app delegate to report the results. You can use that method to determine if your request was granted or denied by the user.

iOS 10 with UserNotifications framework

In iOS 10, Apple introduced UserNotifications and UserNotificationsUI framework and lots of new features to push notifications like actions and attachments.

To ask for permission to present push message from iOS 10, use the new UNUserNotificationCenter which accepts options and block callback with grant status.

1
UNUserNotificationCenter.current().requestAuthorization(options: [.badge, .sound, .alert])

abc

There ‘s also UNNotificationAction and UNNotificationAttachment to specify additional actions and attachment to go along with the push message, this is very handy for visual purpose and convenient actions user can perform right away from the push message.

There’s also a convenient UserNotificationsUI that was shipped with iOS 10 that allows us to embed custom view controller from our push message

ui

When an iOS device receives a notification containing an alert, the system displays the contents of the alert in two stages. Initially, it displays an abbreviated banner with the title, subtitle, and two to four lines of body text from the notification. If the user presses the abbreviated banner, iOS displays the full notification interface, including any notification-related actions. The system provides the interface for the abbreviated banner, but you can customize the full interface using a notification content app extension.

Also, there is this callback userNotificationCenter _:willPresent that asks the delegate how to handle a notification that arrived while the app was running in the foreground.

If your app is in the foreground when a notification arrives, the shared user notification center calls this method to deliver the notification directly to your app. If you implement this method, you can take whatever actions are necessary to process the notification or show it when your app is running.

iOS 12 with provisional push

New in iOS 12 is the UNAuthorizationStatus.provisional, which are notifications that appear silently in the user’s notification center without appearing on the user’s home screen. We can start sending them as soon as a user has downloaded and run your app for the first time. You can send provisional push notifications unlimited times unless the user explicitly turns them off.

Screenshot 2020-10-11 at 06 58 34

This is good to send unobtrusive push to users in their Notification Center where they can pick up at a later time.

iOS 13 with apns-push-type

Starting with iOS 13 and watchOS 6, there is apns-push-type which must accurately reflect the contents of your notification’s payload. If there is a mismatch, or if the header is missing on required systems, APNs may return an error.

The apns-push-type header field has six valid values. The descriptions below describe when and how to use these values. For example alert for notifications that trigger a user interaction and background for notifications that deliver content in the background.

In a WWDC 2019 session Advances in App Background Execution, apns-priority must be set to 5 for content available notifications.

iOS 14 with ephemeral authorization status and AppClip

From Enable your App Clip to schedule and receive notifications for a short or extended time period.

Some App Clips may need to schedule or receive notifications to provide value. Consider an App Clip that allows users to order food for delivery: By sending notifications, the App Clip informs the user about an upcoming delivery. If notifications are important for your App Clip’s functionality, enable it to schedule or receive notifications for up to 8 hours after each launch.

Remote notification only with content-available

Besides user interface notification, there is content-available notification that delivers notifications that wake your app and update it in the background. If your app’s server-based content changes infrequently or at irregular intervals, you can use background notifications to notify your app when new content becomes available. Read Pushing Background Updates to Your App

Testing push notification in simulator

We have been able to drag images to the Photos app in simulator for years, but new in Xcode 11.4 is the ability to drag push payload to simulator to simulate remote push notification.

All we have to do is create an apns file with Simulator Target Bundle key to specify our app, then drag to the simulator

1
2
3
4
5
6
7
8
{
"Simulator Target Bundle": "com.onmyway133.PushHero",
"aps": {
"alert": "Welcome to Push Hero",
"sound": "chime",
"badge": 2
}
}

Many of the simulator features can be controlled via xcrun simctl command line tool where you can change status bar time, battery info, start and stop certain simulators and send push with xcrun simctl push. This is very handy in case you want to automate things.

Test push notification easily with Push Hero

As iOS developers who need to test push notification a lot, I face this challenge. That’s why I made Push Hero as a native macOS application that allows us to reliably test push notification. It is written in pure Swift with all the new APNs specification in mind.

Screenshot 2020-10-08 at 06 17 12

With Push Hero, we can setup multiple test scenario for different app. Each we can specify the authentication method we want, either with p8 token or p12 certificate based. There’s also input validation and hint helper that explains which field is needed and in which format, so you save time to work on your push testing instead.

New in latest version of Push Hero is the ability to send multiple pushes to multiple device tokens, which is the most frequent request. In the right history pane, there’s information about each request and response content, together with apns id information.

Also in Push Hero is the popup to show explanation for each field, and you need to consult Sending Notification Requests to APNs documentation as there are some specifications there. For example with VoIP push, the apns-topic header field must use your app’s bundle ID with .voip appended to the end. If you’re using certificate-based authentication, you must also register the certificate for VoIP services

Conclusion

Push notification continues to be important for iOS apps, and Apple has over the years improved and changed it for the better. This also means lots of knowledge to keep up with. Understanding provider APNs, setting up certificate and JSON Web Token key can be intimidating and take time.

Hopefully the above summary gets you more understanding into push notification, not only the history of changes in both client and provider API, but also some ways to test it easily.


Updated at 2021-01-01 23:26:17

How to style multiline Text in SwiftUI for macOS

Issue #681

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

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

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

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

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

Updated at 2020-10-07 05:02:01

How to clear List background color in SwiftUI for macOS

Issue #680

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

The solution is to use a library like SwiftUI-Introspect

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

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

then

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

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

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

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

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

The app launches briefly and then quits without error message.

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

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

Updated at 2020-10-09 03:49:39

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

Issue #679

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

Screenshot 2020-10-01 at 06 58 33

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
extension View {
func sectionBackground(_ title: String, _ shows: Binding<Bool>) -> some View {
VStack(alignment: .leading) {
HStack {
Text(title.uppercased())
Spacer()
if shows != nil {
SmallButton(
imageName: "downArrow",
tooltip: shows!.wrappedValue ? "Collapse" : "Expand",
action: {
withAnimation(.easeInOut) {
shows!.wrappedValue.toggle()
}
}
)
.rotationEffect(.radians(shows!.wrappedValue ? .pi : 0))
}
}

if shows.wrappedValue {
self
}
}
}
}

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

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

How to dynamically add items to VStack from list in SwiftUI

Issue #678

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

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

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

How to unwrap Binding with Optional in SwiftUI

Issue #677

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

1
2
3
4
5
6
7
import SwiftUI

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

// New props
var notificationId: String?

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

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

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

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

so we can use them as normal

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

How to use Binding in function in Swift

Issue #675

Use wrappedValue to get the underlying value that Binding contains

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

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

Issue #674

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

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

struct MainView: View {
@EnvironmentObject var store: Store

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

Updated at 2020-09-23 08:42:29

How to draw arc corner using Bezier Path

Issue #673

To draw rounded 2 corners at top left and top right, let’s start from bottom left

1
2
3
4
5
6
7
8
9
10
let path = UIBezierPath()
// bottom left
path.move(to: CGPoint(x: 0, y: bounds.height))
// top left corner
path.addArc(withCenter: CGPoint(x: radius, y: radius), radius: radius, startAngle: CGFloat.pi, endAngle: CGFloat.pi * 3 / 2, clockwise: true)
// top right corner
path.addArc(withCenter: CGPoint(x: bounds.width - radius, y: radius), radius: radius, startAngle: CGFloat.pi * 3 / 2, endAngle: 0, clockwise: true)
// bottom right
path.addLine(to: CGPoint(x: bounds.width, y: bounds.height))
path.close()
Screenshot 2020-09-15 at 14 16 01

cc

Read more


Updated at 2020-09-15 12:18:11

How to stitch and sort array in Swift

Issue #672

Supposed we want to stitch magazines array into books array. The requirement is to sort them by publishedDate, but must keep preferredOrder of books. One way to solve this is to declare an enum to hold all possible cases, and then do a sort that check every possible combination

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
struct Book {
let preferredOrder: Int
let publishedDate: Date
}

struct Magazine {
let publishedDate: Date
}

enum StitchItem {
case book(Book)
case magazine(Magazine)
}

func stitch(_ books: [Book], magazines: [Magazine]) -> [StitchItem] {
let items = books.map({ StitchItem.book($0) }) + magazines.map({ StitchItem.magazine($0) })
return items.sorted(by: { book, magazine in
switch (book, magazine) {
case let (.book(b1), .book(b2)):
return b1.preferredOrder < b2.preferredOrder
case let (.book(book), .magazine(magazine)):
if book.publishedDate == magazine.publishedDate {
return true
} else {
return book.publishedDate < magazine.publishedDate
}
case let (.magazine(magazine), .book(book)):
if book.publishedDate == magazine.publishedDate {
return false
} else {
return book.publishedDate < magazine.publishedDate
}
case let (.magazine(m1), .magazine(m2)):
return m1.publishedDate < m2.publishedDate
}
})
}

The above sort function declares the intention but Swift just sort instead of trying to fully meet our requirements.

A manual solution is to sort each array first then use while loop to insert.

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
func stitch(_ books: [Book], magazines: [Magazine]) -> [StitchItem] {
let books = books
.sorted(by: { $0.preferredOrder < $1.preferredOrder })
let magazines = magazines
.sorted(by: sortmagazines)

var bookIndex = 0
var magazineIndex = 0
var results: [StitchItem] = []
while (bookIndex < books.count && magazineIndex < magazines.count) {
let book = books[bookIndex]
let magazine = magazines[magazineIndex]
if book.publishedDate < magazine.publishedDate {
results.append(StitchItem.book(book))
bookIndex += 1
} else {
results.append(StitchItem.magazine(magazine))
magazineIndex += 1
}
}

while (bookIndex < books.count) {
let book = books[bookIndex]
results.append(StitchItem.book(book))
bookIndex += 1
}

while (magazineIndex < magazines.count) {
let magazine = magazines[magazineIndex]
results.append(StitchItem.magazine(magazine))
magazineIndex += 1
}

return results
}

Updated at 2020-08-31 12:19:33

How to make dynamic font size for UIButton

Issue #671

Use adjustsFontForContentSizeCategory

A Boolean that indicates whether the object automatically updates its font when the device’s content size category changes.

If you set this property to YES, the element adjusts for a new content size category on a UIContentSizeCategoryDidChangeNotification.

1
2
3
4
5
6
7
button.titleLabel?.adjustsFontForContentSizeCategory = true
button.backgroundColor = UIColor.green
button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .title1)

label.adjustsFontForContentSizeCategory = true
label.backgroundColor = UIColor.yellow
label.font = UIFont.preferredFont(forTextStyle: .title1)

However it seems view (UIButton or UILabel) size is the same, just the inner text increases in size. A workaround is to put view inside UIStackView so UIButton or UILabel can automatically changes size.

How to test for view disappear in navigation controller

Issue #670

To test for viewWillDisappear during UINavigationController popViewController in unit test, we need to simulate UIWindow so view appearance works.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
final class PopTests: XCTestCase {
func testPop() {
let window = UIWindow(frame: UIScreen.main.bounds)
let navigationController = UINavigationController()
window.rootViewController = navigationController
let viewController = DetailViewController()

navigationController.viewControllers = [
UIViewController(),
viewController
]

window.makeKeyAndVisible()
let expectation = XCTestExpectation()
navigationController.popViewController(animated: false)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
XCTAssertTrue(viewController.wasDismissed)
expectation.fulfill()
}
wait(for: [expectation], timeout: 1)
}
}
1
2
3
4
5
6
7
8
class DetailViewController: UIViewController {
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if isMovingFromParent {
wasDismissed = true
}
}
}

Updated at 2020-08-14 07:30:07