How to make simple search box in iOS

Issue #227

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
final class SearchBox: UIView {
lazy var textField: UITextField = {
let textField = UITextField()
let imageView = UIImageView(image: R.image.search()!)
imageView.frame.size = CGSize(width: 20 + 8, height: 20)
imageView.contentMode = .scaleAspectFit
textField.leftView = imageView
textField.leftViewMode = .always

return textField
}()

lazy var filterButton: UIButton = {
let button = UIButton(type: .custom)
button.setImage(R.image.filter()!, for: .normal)
button.imageEdgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)

return button
}()

lazy var backView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.layer.cornerRadius = 10
view.layer.borderColor = R.color.lightGray.cgColor
view.layer.borderWidth = 0.5
view.layer.shadowOffset = CGSize(width: 1, height: 1)
view.layer.shadowOpacity = 0.8
view.layer.shadowColor = R.color.lightGray.cgColor

return view
}()

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

setup()
}

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

private func setup() {
addSubviews([backView, textField, filterButton])

NSLayoutConstraint.on([
filterButton.rightAnchor.constraint(equalTo: rightAnchor, constant: -8),
filterButton.centerYAnchor.constraint(equalTo: centerYAnchor),
filterButton.heightAnchor.constraint(equalToConstant: 44),
filterButton.widthAnchor.constraint(equalToConstant: 44),

textField.centerYAnchor.constraint(equalTo: centerYAnchor),
textField.leftAnchor.constraint(equalTo: leftAnchor, constant: 16),
textField.rightAnchor.constraint(equalTo: filterButton.leftAnchor, constant: -8)
])

NSLayoutConstraint.on([
backView.pinEdges(view: self, inset: UIEdgeInsets(top: 4, left: 4, bottom: -4, right: -4))
])
}
}

To apply padding to leftView, increase width and use contentMode

1
2
imageView.frame.size = CGSize(width: 20 + 8, height: 20)
imageView.contentMode = .scaleAspectFit

To make image in button smaller, use imageEdgeInsets with all positive values
To have round and shadow, specify shadowOpacity, cornerRadius, shadowOffset

searchbox

How to capture video in iOS simulator

Issue #226

Take screenshot

1
xcrun simctl io booted screenshot image.png

Record video

1
xcrun simctl io booted recordVideo video.mp4

How to use custom fonts in iOS

Issue #225

1
2
3
4
5
6
7
8
9
10
11
12
13
<key>UIAppFonts</key>
<array>
<string>OpenSans-Bold.ttf</string>
<string>OpenSans-BoldItalic.ttf</string>
<string>OpenSans-ExtraBold.ttf</string>
<string>OpenSans-ExtraBoldItalic.ttf</string>
<string>OpenSans-Italic.ttf</string>
<string>OpenSans-Light.ttf</string>
<string>OpenSans-LightItalic.ttf</string>
<string>OpenSans-Regular.ttf</string>
<string>OpenSans-SemiBold.ttf</string>
<string>OpenSans-SemiBoldItalic.ttf</string>
</array>

Read Adding a Custom Font to Your App

The name of the font isn’t always obvious, and rarely matches the font file name. A quick way to find the font name is to get the list of fonts available to your app, which you can do with the following code:

1
2
3
4
for family in UIFont.familyNames.sorted() {
let names = UIFont.fontNames(forFamilyName: family)
print("Family: \(family) Font names: \(names)")
}

How to create UITabBarController programmatically in iOS

Issue #224

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let tabBarController = UITabBarController()

let navigationController1 = UINavigationController(rootViewController: viewController1)
let navigationController2 = UINavigationController(rootViewController: viewController2)
let navigationController3 = UINavigationController(rootViewController: viewController3)

navigationController2.isNavigationBarHidden = true

navigationController1.tabBarItem.image = R.image.profile()
navigationController2.tabBarItem.image = R.image.books()
navigationController3.tabBarItem.image = R.image.settings()

tabBarController.tabBar.tintColor = .yellow
tabBarController.viewControllers = [navigationController1, navigationController2, navigationController3]

Use tintColor instead of the deprecated selectedImageTintColor to indicate selected item color.

For icon size, check Tab Bar Icon Size, usually 50x50 for 2x and 75x75 for 3x

In portrait orientation, tab bar icons appear above tab titles. In landscape orientation, the icons and titles appear side-by-side. Depending on the device and orientation, the system displays either a regular or compact tab bar. Your app should include custom tab bar icons for both sizes.

tab

How to make simple networking client in Swift

Issue #222

For more mature networking, visit https://github.com/onmyway133/Miami

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
final class NetworkClient {
let session: URLSession
let baseUrl: URL

init(session: URLSession = .shared, baseUrl: URL) {
self.session = session
self.baseUrl = baseUrl
}

func make(options: Options, completion: @escaping (Result<Data, Error>) -> Void) {
guard let request = options.toRequest(baseUrl: baseUrl) else {
completion(.failure(AppError.request))
return
}

let task = session.dataTask(with: request, completionHandler: { data, _, error in
if let data = data {
completion(.success(data))
} else if let error = error {
completion(.failure(error))
} else {
completion(.failure(AppError.unknown))
}
})

task.resume()
}

func makeJson(options: Options, completion: @escaping (Result<[String: Any], Error>) -> Void) {
make(options: options, completion: { result in
let mapped = result.flatMap({ data -> Result<[String: Any], Error> in
do {
let json = try JSONSerialization.jsonObject(with: data, options: [])
if let json = json as? [String: Any] {
return Result<[String: Any], Error>.success(json)
} else {
return Result<[String: Any], Error>.failure(AppError.parse)
}
} catch {
return Result<[String: Any], Error>.failure(error)
}
})

completion(mapped)
})
}
}

struct Options {
var path: String = ""
var httpMethod: HttpMethod = .get
var parameters: [String: Any] = [:]

func toRequest(baseUrl: URL) -> URLRequest? {
let url = baseUrl.appendingPathComponent(path)
let items: [URLQueryItem] = parameters.map({ tuple -> URLQueryItem in
return URLQueryItem(name: tuple.key, value: "\(tuple.value)")
})

var components = URLComponents(url: url, resolvingAgainstBaseURL: false)

if httpMethod == .get {
components?.queryItems = items
}

guard let finalUrl = components?.url else {
return nil
}

var request = URLRequest(url: finalUrl)

if httpMethod == .post {
let data = try? JSONSerialization.data(withJSONObject: parameters, options: [])
request.httpBody = data
}

request.httpMethod = httpMethod.rawValue
return request
}
}

enum AppError: Error {
case request
case unknown
case parse
}

enum HttpMethod: String {
case get = "GET"
case put = "PUT"
case post = "POST"
case patch = "PATCH"
}

How to use Stripe and Apple Pay in iOS

Issue #219

Show basic add card in iOS

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

final class MainController: UIViewController {

func showPayment() {
let addCardViewController = STPAddCardViewController()
addCardViewController.delegate = self
let navigationController = UINavigationController(rootViewController: addCardViewController)

present(navigationController, animated: true, completion: nil)
}
}

extension MainController: STPAddCardViewControllerDelegate {
func addCardViewControllerDidCancel(_ addCardViewController: STPAddCardViewController) {
dismiss(animated: true, completion: nil)
}

func addCardViewController(_ addCardViewController: STPAddCardViewController, didCreateToken token: STPToken, completion: @escaping STPErrorBlock) {
_ = token.tokenId
completion(nil)
dismiss(animated: true, completion: nil)
}
}

Generate ephemeral key

https://stripe.com/docs/mobile/ios/standard#ephemeral-key

In order for our prebuilt UI elements to function, you’ll need to provide them with an ephemeral key, a short-lived API key with restricted API access. You can think of an ephemeral key as a session, authorizing the SDK to retrieve and update a specific Customer object for the duration of the session.

Backend in Go

https://github.com/stripe/stripe-go

Need a secret key by going to Stripe dashboard -> Developers -> API keys -> Secret key

1
stripe.Key = "sk_key"

Need customer id. We can manually create one in Stripe dashboard -> Customers

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
package main

import (
"net"
"encoding/json"
"fmt"
"net/http"
"github.com/stripe/stripe-go"
"github.com/stripe/stripe-go/ephemeralkey"
)

func main() {
stripe.Key = "sk_test_mM2MkqO61n7vvbVRfeYmBgWm00Si2PtWab"

http.HandleFunc("/ephemeral_keys", generateEphemeralKey)
if err := http.ListenAndServe(":8080", nil); err != nil {
panic(err)
}
}

type EphemeralKeysRequest struct {
ApiVersion string `json:"api_version"`
}

func generateEphemeralKey(w http.ResponseWriter, r *http.Request) {
customerId := "cus_Eys6aeP5xR89ab"

decoder := json.NewDecoder(r.Body)
var t EphemeralKeysRequest
err := decoder.Decode(&t)
if err != nil {
panic(err)
}

stripeVersion := t.ApiVersion
if stripeVersion == "" {
log.Printf("Stripe-Version not found\n")
w.WriteHeader(400)
return
}
params := &stripe.EphemeralKeyParams{
Customer: stripe.String(customerId),
StripeVersion: stripe.String(stripeVersion),
}

key, err := ephemeralkey.New(params)
if err != nil {
log.Printf("Stripe bindings call failed, %v\n", err)
w.WriteHeader(500)
return
}
w.Write(key.RawJSON)
}

iOS client

Networking client uses How to make simple networking client in Swift

Need an object that conforms to STPCustomerEphemeralKeyProvider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
final class EphemeralKeyClient: NSObject, STPCustomerEphemeralKeyProvider {
let client = NetworkClient(baseUrl: URL(string: "http://localhost:8080")!)

func createCustomerKey(withAPIVersion apiVersion: String, completion: @escaping STPJSONResponseCompletionBlock) {
var options = Options()
options.httpMethod = .post
options.path = "ephemeral_keys"
options.parameters = [
"api_version": apiVersion
]

client.makeJson(options: options, completion: { result in
switch result {
case .success(let json):
completion(json, nil)
case .failure(let error):
completion(nil, error)
}
})
}
}

Setting up STPCustomerContext and STPPaymentContext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
final class MainController: UIViewController {
let client = EphemeralKeyClient()
let customerContext: STPCustomerContext
let paymentContext: STPPaymentContext

init() {
self.customerContext = STPCustomerContext(keyProvider: client)
self.paymentContext = STPPaymentContext(customerContext: customerContext)
super.init(nibName: nil, bundle: nil)
paymentContext.delegate = self
paymentContext.hostViewController = self
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

func start() {
paymentContext.presentShippingViewController()
}
}

Handle charge

https://stripe.com/docs/charges

Backend in Go

If we use stripe_id from card, which has the form of card_xxx, we need to include customer info

If we use token, which has the form tok_xxx, then no need for customer info

From STPPaymentResult

When you’re using STPPaymentContext to request your user’s payment details, this is the object that will be returned to your application when they’ve successfully made a payment. It currently just contains a source, but in the future will include any relevant metadata as well. You should pass source.stripeID to your server, and call the charge creation endpoint. This assumes you are charging a Customer, so you should specify the customer parameter to be that customer’s ID and the source parameter to the value returned here. For more information, see https://stripe.com/docs/api#create_charge

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
package main

import (
"net"
"encoding/json"
"fmt"
"net/http"
"log"
"os"
"github.com/stripe/stripe-go/charge"
)

func main() {
stripe.Key = "sk_test_mM2MkqO61n7vvbVRfeYmBgWm00Si2PtWab"

http.HandleFunc("/request_charge", handleCharge)
if err := http.ListenAndServe(":8080", nil); err != nil {
panic(err)
}
}

var customerId = "cus_Eys6aeP5xR89ab"

type PaymentResult struct {
StripeId string `json:"stripe_id"`
}

func handleCharge(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body)
var t PaymentResult
err := decoder.Decode(&t)
if err != nil {
panic(err)
}

params := &stripe.ChargeParams{
Amount: stripe.Int64(150),
Currency: stripe.String(string(stripe.CurrencyUSD)),
Description: stripe.String("Charge from my Go backend"),
Customer: stripe.String(customerId),
}

params.SetSource(t.StripeId)
ch, err := charge.New(params)
if err != nil {
fmt.Fprintf(w, "Could not process payment: %v", err)
fmt.Println(ch)
w.WriteHeader(400)
}

w.WriteHeader(200)
}

iOS client

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
final class PaymentClient {
let client = NetworkClient(baseUrl: URL(string: "http://192.168.210.219:8080")!)

func requestCharge(source: STPSourceProtocol, completion: @escaping (Result<(), Error>) -> Void) {
var options = Options()
options.httpMethod = .post
options.path = "request_charge"
options.parameters = [
"stripe_id": source.stripeID
]

client.makeJson(options: options, completion: { result in
completion(result.map({ _ in () }))
})
}
}

paymentContext.requestPayment()

extension MainController: STPPaymentContextDelegate {
func paymentContext(_ paymentContext: STPPaymentContext, didCreatePaymentResult paymentResult: STPPaymentResult, completion: @escaping STPErrorBlock) {
client.requestCharge(source: paymentResult.source, completion: { result in
switch result {
case .success:
completion(nil)
case .failure(let error):
completion(error)
}
})
}
}

Token from card

Use STPAPIClient.shared().createToken to get token from card https://stripe.com/docs/mobile/ios/custom#collecting-card-details

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let cardParams = STPCardParams()
cardParams.number = "4242424242424242"
cardParams.expMonth = 10
cardParams.expYear = 2021
cardParams.cvc = "123"

STPAPIClient.shared().createToken(withCard: cardParams) { (token: STPToken?, error: Error?) in
guard let token = token, error == nil else {
// Present error to user...
return
}

submitTokenToBackend(token, completion: { (error: Error?) in
if let error = error {
// Present error to user...
}
else {
// Continue with payment...
}
})
}

Payment options and shipping view controllers

Instead of using paymentContext

1
2
paymentContext.pushShippingViewController()
paymentContext.pushPaymentOptionsViewController()

We can use view controllers https://stripe.com/docs/mobile/ios/custom#stppaymentoptionsviewcontroller directly with STPPaymentOptionsViewController and STPShippingAddressViewController. Then implement STPPaymentOptionsViewControllerDelegate and STPShippingAddressViewControllerDelegate

Register merchant Id and Apple Pay certificate

https://stripe.com/docs/apple-pay/apps

Get Certificate signing request file from Stripe https://dashboard.stripe.com/account/apple_pay

We can’t register merchant id with Enterprise account

Use Apple Pay

Go backend

Use token

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
type ApplePayRequest struct {
Token string `json:"token"`
}

func handleChargeUsingApplePay(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body)
var t ApplePayRequest
err := decoder.Decode(&t)
if err != nil {
panic(err)
}

params := &stripe.ChargeParams{
Amount: stripe.Int64(150),
Currency: stripe.String(string(stripe.CurrencyUSD)),
Description: stripe.String("Charge from my Go backend for Apple Pay"),
}

params.SetSource(t.Token)
ch, err := charge.New(params)
if err != nil {
fmt.Fprintf(w, "Could not process payment: %v", err)
fmt.Println(ch)
w.WriteHeader(400)
}

w.WriteHeader(200)
}

iOS client

Update client to send STPToken

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
final class PaymentClient: NSObject {
let client = NetworkClient(baseUrl: URL(string: "localhost:8080")!)

func requestCharge(token: STPToken, completion: @escaping (Result<(), Error>) -> Void) {
var options = Options()
options.httpMethod = .post
options.path = "request_charge_apple_pay"
options.parameters = [
"token": token.tokenId
]

client.make(options: options, completion: { result in
completion(result.map({ _ in () }))
})
}

func useApplePay(payment: PKPayment, completion: @escaping (Result<(), Error>) -> Void) {
STPAPIClient.shared().createToken(with: payment, completion: { (token: STPToken?, error: Error?) in
guard let token = token, error == nil else {
return
}

self.requestCharge(token: token, completion: completion)
})
}
}

Use PKPaymentAuthorizationViewController, not PKPaymentAuthorizationController

https://developer.apple.com/documentation/passkit/pkpaymentauthorizationcontroller

The PKPaymentAuthorizationController class performs the same role as the PKPaymentAuthorizationViewController class, but it does not depend on the UIKit framework. This means that the authorization controller can be used in places where a view controller cannot (for example, in watchOS apps or in SiriKit extensions).

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
extension MainController {
func showApplePay() {
let merchantId = "merchant.com.onmyway133.MyApp"
let paymentRequest = Stripe.paymentRequest(withMerchantIdentifier: merchantId, country: "US", currency: "USD")
paymentRequest.paymentSummaryItems = [
PKPaymentSummaryItem(label: "Rubber duck", amount: 1.5)
]

guard Stripe.canSubmitPaymentRequest(paymentRequest) else {
assertionFailure()
return
}

guard let authorizationViewController = PKPaymentAuthorizationViewController(paymentRequest: paymentRequest) else {
assertionFailure()
return
}

authorizationViewController.delegate = self
innerNavigationController.present(authorizationViewController, animated: true, completion: nil)
}
}

extension MainController: PKPaymentAuthorizationViewControllerDelegate {
func paymentAuthorizationViewControllerDidFinish(_ controller: PKPaymentAuthorizationViewController) {
controller.dismiss(animated: true, completion: nil)
}

func paymentAuthorizationViewController(
_ controller: PKPaymentAuthorizationViewController,
didAuthorizePayment payment: PKPayment,
handler completion: @escaping (PKPaymentAuthorizationResult) -> Void) {

client.useApplePay(payment: payment, completion: { result in
switch result {
case .success:
completion(.init(status: .success, errors: nil))
case .failure(let error):
completion(.init(status: .failure, errors: [error]))
}
})
}
}

Showing Apple Pay option

From appleMerchantIdentifier

The Apple Merchant Identifier to use during Apple Pay transactions. To create one of these, see our guide at https://stripe.com/docs/mobile/apple-pay . You must set this to a valid identifier in order to automatically enable Apple Pay.

1
2
3
4
5
if Stripe.deviceSupportsApplePay() {
STPPaymentConfiguration.shared().appleMerchantIdentifier = "merchant.com.onmyway133.MyApp"
}

paymentContext.pushPaymentOptionsViewController()

requestPayment not showing UI

From requestPayment

Requests payment from the user. This may need to present some supplemental UI to the user, in which case it will be presented on the payment context’s hostViewController. For instance, if they’ve selected Apple Pay as their payment method, calling this method will show the payment sheet. If the user has a card on file, this will use that without presenting any additional UI. After this is called, the paymentContext:didCreatePaymentResult:completion: and paymentContext:didFinishWithStatus:error: methods will be called on the context’s delegate.

Use STPPaymentOptionsViewController to show cards and Apple Pay options

Code for requestPayment

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
- (void)requestPayment {
WEAK(self);
[[[self.didAppearPromise voidFlatMap:^STPPromise * _Nonnull{
STRONG(self);
return self.loadingPromise;
}] onSuccess:^(__unused STPPaymentOptionTuple *tuple) {
STRONG(self);
if (!self) {
return;
}

if (self.state != STPPaymentContextStateNone) {
return;
}

if (!self.selectedPaymentOption) {
[self presentPaymentOptionsViewControllerWithNewState:STPPaymentContextStateRequestingPayment];
}
else if ([self requestPaymentShouldPresentShippingViewController]) {
[self presentShippingViewControllerWithNewState:STPPaymentContextStateRequestingPayment];
}
else if ([self.selectedPaymentOption isKindOfClass:[STPCard class]] ||
[self.selectedPaymentOption isKindOfClass:[STPSource class]]) {
self.state = STPPaymentContextStateRequestingPayment;
STPPaymentResult *result = [[STPPaymentResult alloc] initWithSource:(id<STPSourceProtocol>)self.selectedPaymentOption];
[self.delegate paymentContext:self didCreatePaymentResult:result completion:^(NSError * _Nullable error) {
stpDispatchToMainThreadIfNecessary(^{
if (error) {
[self didFinishWithStatus:STPPaymentStatusError error:error];
} else {
[self didFinishWithStatus:STPPaymentStatusSuccess error:nil];
}
});
}];
}
else if ([self.selectedPaymentOption isKindOfClass:[STPApplePayPaymentOption class]]) {
// ....

Payment options

1
2
3
func paymentOptionsViewController(_ paymentOptionsViewController: STPPaymentOptionsViewController, didSelect paymentOption: STPPaymentOption) {
// No op
}

After user selects payment option, the change is saved in dashboard https://dashboard.stripe.com/test/customers, but for card only. Select Apple Pay does not reflect change in web dashboard.

Apple pay option is added manually locally, from STPCustomer+SourceTuple.m 😲

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
- (STPPaymentOptionTuple *)filteredSourceTupleForUIWithConfiguration:(STPPaymentConfiguration *)configuration {
id<STPPaymentOption> _Nullable selectedMethod = nil;
NSMutableArray<id<STPPaymentOption>> *methods = [NSMutableArray array];
for (id<STPSourceProtocol> customerSource in self.sources) {
if ([customerSource isKindOfClass:[STPCard class]]) {
STPCard *card = (STPCard *)customerSource;
[methods addObject:card];
if ([card.stripeID isEqualToString:self.defaultSource.stripeID]) {
selectedMethod = card;
}
}
else if ([customerSource isKindOfClass:[STPSource class]]) {
STPSource *source = (STPSource *)customerSource;
if (source.type == STPSourceTypeCard
&& source.cardDetails != nil) {
[methods addObject:source];
if ([source.stripeID isEqualToString:self.defaultSource.stripeID]) {
selectedMethod = source;
}
}
}
}

return [STPPaymentOptionTuple tupleWithPaymentOptions:methods
selectedPaymentOption:selectedMethod
addApplePayOption:configuration.applePayEnabled];
}

STPApplePayPaymentOptionis not available inpaymentContext.paymentOptions` immediately

Change selected payment option

In STPPaymentContext

setSelectedPaymentOption is read only and trigger paymentContextDidChange, but it checks if the new selected payment option is equal to existing selected payment option

1
2
3
4
5
6
7
8
9
10
11
- (void)setSelectedPaymentOption:(id<STPPaymentOption>)selectedPaymentOption {
if (selectedPaymentOption && ![self.paymentOptions containsObject:selectedPaymentOption]) {
self.paymentOptions = [self.paymentOptions arrayByAddingObject:selectedPaymentOption];
}
if (![_selectedPaymentOption isEqual:selectedPaymentOption]) {
_selectedPaymentOption = selectedPaymentOption;
stpDispatchToMainThreadIfNecessary(^{
[self.delegate paymentContextDidChange:self];
});
}
}

There is retryLoading which is called at init

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
- (void)retryLoading {
// Clear any cached customer object before refetching
if ([self.apiAdapter isKindOfClass:[STPCustomerContext class]]) {
STPCustomerContext *customerContext = (STPCustomerContext *)self.apiAdapter;
[customerContext clearCachedCustomer];
}
WEAK(self);
self.loadingPromise = [[[STPPromise<STPPaymentOptionTuple *> new] onSuccess:^(STPPaymentOptionTuple *tuple) {
STRONG(self);
self.paymentOptions = tuple.paymentOptions;
self.selectedPaymentOption = tuple.selectedPaymentOption;
}] onFailure:^(NSError * _Nonnull error) {
STRONG(self);
if (self.hostViewController) {
[self.didAppearPromise onSuccess:^(__unused id value) {
if (self.paymentOptionsViewController) {
[self appropriatelyDismissPaymentOptionsViewController:self.paymentOptionsViewController completion:^{
[self.delegate paymentContext:self didFailToLoadWithError:error];
}];
} else {
[self.delegate paymentContext:self didFailToLoadWithError:error];
}
}];
}
}];
[self.apiAdapter retrieveCustomer:^(STPCustomer * _Nullable customer, NSError * _Nullable error) {
stpDispatchToMainThreadIfNecessary(^{
STRONG(self);
if (!self) {
return;
}
if (error) {
[self.loadingPromise fail:error];
return;
}
if (!self.shippingAddress && customer.shippingAddress) {
self.shippingAddress = customer.shippingAddress;
self.shippingAddressNeedsVerification = YES;
}

STPPaymentOptionTuple *paymentTuple = [customer filteredSourceTupleForUIWithConfiguration:self.configuration];

[self.loadingPromise succeed:paymentTuple];
});
}];
}

Which in turns call STPCustomerEphemeralKeyProvider. As stripe does not save Apple Pay option in dashboard, this method return list of card payment options, together with the default card as selected payment option 😲

Although the new STPCard has a different address, it is the exact same card with the same info, and the isEqual method of STPCard is

1
2
3
4
5
6
7
8
9
10
11
- (BOOL)isEqualToCard:(nullable STPCard *)other {
if (self == other) {
return YES;
}

if (!other || ![other isKindOfClass:self.class]) {
return NO;
}

return [self.stripeID isEqualToString:other.stripeID];
}

I raised an issue How to change selected payment option? hope it gets resolved soon 😢

How to test PublishSubject in RxSwift

Issue #218

Use homemade Recorder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Recorder<T> {
var items = [T]()
let bag = DisposeBag()

func on(arraySubject: PublishSubject<[T]>) {
arraySubject.subscribe(onNext: { value in
self.items = value
}).disposed(by: bag)
}

func on(valueSubject: PublishSubject<T>) {
valueSubject.subscribe(onNext: { value in
self.items.append(value)
}).disposed(by: bag)
}
}

Then test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
final class BookViewModelTests: XCTestCase {
func testBooks() throws {
let expectation = self.expectation(description: #function)
let recorder = Recorder<Book>()
let viewModel = BookViewModel(bookClient: MockBookClient())
recorder.on(arraySubject: viewModel.books)
viewModel.load()

DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5, execute: {
expectation.fulfill()
})

wait(for: [expectation], timeout: 2)
XCTAssertEqual(recorder.items.count, 3)
}
}

Need to use great timeout value as DispatchQueue is not guaranteed to be precise, a block needs to wait for the queue to be empty before it can be executed

Make expectation less cumbersome

1
2
3
4
5
6
7
8
9
10
11
extension XCTestCase {
func waitOrFail(timeout: TimeInterval) {
let expectation = self.expectation(description: #function)

DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + timeout, execute: {
expectation.fulfill()
})

wait(for: [expectation], timeout: timeout + 2)
}
}

How to use Sonarqube in Swift projects

Issue #216

Install Sonarqube

https://docs.sonarqube.org/latest/setup/get-started-2-minutes/

  • Download Sonarqube for macOS https://www.sonarqube.org/downloads/
  • Put it in ~/sonarqube
  • Run localhost server ~/sonarqube/bin/macosx-universal-64/sonar.sh console
  • Login http://localhost:9000 with admin/admin
  • Create new project

Install Sonar scanner

https://docs.sonarqube.org/display/SCAN/Analyzing+with+SonarQube+Scanner

  • Download for macOS 64 bit
  • Put it in ~/sonarscanner
  • export PATH=$PATH:/Users/khoa/sonarscanner/bin
  • Go to project, create sonar-project.properties
1
2
3
4
5
6
7
8
9
10
11
12
# must be unique in a given SonarQube instance
sonar.projectKey=my-app
# this is the name and version displayed in the SonarQube UI. Was mandatory prior to SonarQube 6.1.
sonar.projectName=My App
sonar.projectVersion=1.0

# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows.
# This property is optional if sonar.modules is set.
sonar.sources=.

# Encoding of the source code. Default is default system encoding
#sonar.sourceEncoding=UTF-8
  • Run sonar-scanner

Install swift plugin

https://github.com/Backelite/sonar-swift

a

Skip some tools

Modify run-sonar-swift.sh

1
2
3
4
5
6
7
8
9
vflag=""
nflag=""
unittests="on"
swiftlint="on"
tailor="off"
lizard="on"
oclint="off"
fauxpas="off"
sonarscanner=""

Git ignore

1
2
3
.scannerwork/
sonar-reports/
compile_commands.json

run-sonar-swift.sh

  • Update sonar-project.properties
1
2
3
4
sonar.swift.appScheme=MyApp Staging
sonar.swift.project=MyApp.xcodeproj
sonar.swift.workspace=MyApp.xcworkspace
sonar.swift.simulator=platform=iOS Simulator,name=iPhone Xʀ
  • Run chmod +x run-sonar-swift.sh
  • Run ./run-sonar-swift.sh

Troubleshooting

failed with error code: 64 https://github.com/Backelite/sonar-swift/issues/222

When run ./run-sonar-swift.sh

1
2
3
4
xcodebuild: error: ''MyApp.xcodeproj'' does not exist.
2019-04-29 12:10:17.486 defaults[4134:569992]
Domain CFBundleShortVersionString does not exist
.Extracting Xcode project informationxcodebuild: error: option 'Destination' requires at least one parameter of the form 'key=value'

👉 Remove quotes in sonar-project.properties
👉 Modify run-sonar-swift.sh, add these before Check for mandatory parameters section

Surround by double quotes

1
2
3
4
projectFile="\"$projectFile\""
workspaceFile="\"$workspaceFile\""
appScheme="\"$appScheme\""
destinationSimulator="\"$destinationSimulator\""

😢 Does not work
👉 Need to create a scheme name without space

Error code 65

👉Specify team in Xcode project

destination

Need double quotes

destinationSimulator="\"$destinationSimulator\""

Use equal sign =

1
-destination="$destinationSimulator"

instead of space ‘ ‘

1
-destination "$destinationSimulator"

Metric ‘files’ should not be computed by a Sensor

When run sonar-scanner https://github.com/Backelite/sonar-swift/issues/212

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
11:01:14.406 INFO: Sensor JaCoCo XML Report Importer [jacoco]
11:01:14.409 DEBUG: No reports found
11:01:14.409 INFO: Sensor JaCoCo XML Report Importer [jacoco] (done) | time=3ms
11:01:14.409 INFO: Sensor SwiftLint [backelitesonarswiftplugin]
11:01:14.417 INFO: Sensor SwiftLint [backelitesonarswiftplugin] (done) | time=8ms
11:01:14.417 INFO: Sensor Tailor [backelitesonarswiftplugin]
11:01:14.418 INFO: Sensor Tailor [backelitesonarswiftplugin] (done) | time=1ms
11:01:14.418 INFO: Sensor OCLint [backelitesonarswiftplugin]
11:01:14.419 INFO: Sensor OCLint [backelitesonarswiftplugin] (done) | time=1ms
11:01:14.419 INFO: Sensor FauxPas [backelitesonarswiftplugin]
11:01:14.419 INFO: Sensor FauxPas [backelitesonarswiftplugin] (done) | time=0ms
11:01:14.419 INFO: Sensor Swift Squid [backelitesonarswiftplugin]
11:01:14.526 INFO: ------------------------------------------------------------------------
11:01:14.526 INFO: EXECUTION FAILURE
11:01:14.526 INFO: ------------------------------------------------------------------------
11:01:14.527 INFO: Total time: 6.180s
11:01:14.603 INFO: Final Memory: 25M/566M
11:01:14.603 INFO: ------------------------------------------------------------------------
11:01:14.603 ERROR: Error during SonarQube Scanner execution
java.lang.UnsupportedOperationException: Metric 'files' should not be computed by a Sensor
at org.sonar.scanner.sensor.DefaultSensorStorage.saveMeasure(DefaultSensorStorage.java:168)
```

👉Install maven https://maven.apache.org/download.cgi
Edit `ObjectiveCSquidSensor.java` and `SwiftSquidSensor`, remove line with `CoreMetrics.FILES`
Run `export PATH=$PATH:/Users/khoa/apache-maven/bin`
Run `./build-and-deploy.sh`
Or `~/apache-maven/bin/mvn clean install`

🎉 Built jar is in `sonar-swift-plugin/target/backelite-sonar-swift-plugin-0.4.4.jar`, copy back to `extensions/plugins`


### How to enable SwiftLint as default profile 🤔

👉 Need to close current Sonar tab and restart server

### Testing failed: unable to attach DB

Modify `run-sonar-swift.sh` to add `-UseModernBuildSystem=NO` to `buildCmd+=(-destination`

### slather No coverage directory found

Try running

slather coverage –input-format profdata –cobertura-xml –output-directory sonar-reports –workspace MyApp.xcworkspace –scheme MyAppStaging MyApp.xcodeproj

1
2
3
4
5
6
7
8

👉 Enable coverage option in scheme -> Test

![](https://github.com/SlatherOrg/slather/raw/master/README_Images/test_scheme.png)

Optional: declare `.slather.yml` file https://github.com/SlatherOrg/slather

### Unable to execute SonarQube

14:53:23.251 ERROR: Error during SonarQube Scanner execution
org.sonarsource.scanner.api.internal.ScannerException: Unable to execute SonarQube
at org.sonarsource.scanner.api.internal.IsolatedLauncherFactory.lambda$createLauncher$0(IsolatedLauncherFactory.java:85)

1
2
3
4

👉Start sonar server

### LizardReportParser$SwiftFunction cannot be cast

Error during SonarQube Scanner execution
java.lang.ClassCastException: com.backelite.sonarqube.swift.complexity.LizardReportParser$SwiftFunction cannot be cast to org.sonar.api.batch.fs.internal.DefaultInputComponent

1
2

👉 Run [lizard](https://github.com/terryyin/lizard) manually

lizard –xml sonar-reports/lizard-report.xml
```

How to map from Swift 5 Resul to RxSwift PublishSubject

Issue #214

1
2
3
4
5
6
7
8
9
10
extension Result {
func to(subject: PublishSubject<Success>) {
switch self {
case .success(let value):
subject.onNext(value)
case .failure(let error):
subject.onError(error)
}
}
}

How to use Timer in Swift

Issue #212

Pre iOS 10

1
2
3
4
5
6
7
8
9
10
func schedule() {
DispatchQueue.main.async {
self.timer = Timer.scheduledTimer(timeInterval: 20, target: self,
selector: #selector(self.timerDidFire(timer:)), userInfo: nil, repeats: false)
}
}

@objc private func timerDidFire(timer: Timer) {
print(timer)
}

iOS 10+

1
2
3
4
5
DispatchQueue.main.async {
self.timer = Timer.scheduledTimer(withTimeInterval: 20, repeats: false) { timer in
print(timer)
}
}

Note that

  • It needs to be on the main queue
  • Callback function can be public, private, …
  • Callback function needs to be @objc

Original answer https://stackoverflow.com/a/42273141/1418457

How to overload functions in Swift

Issue #211

Function

Functions in Swift are distinguishable by

  • parameter label
  • parameter type
  • return type

so that these are all valid, and works for subscript 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
28
struct A {

// return type
func get() -> String { return "" }
func get() -> Int { return 1 }

// mix of parameter type and return type
func get(param: String) -> String { return "" }
func get(param: String) -> Int { return 1 }
func get(param: Int) -> Int { return 1 }
func get(param: Int) -> String { return "" }

subscript(param: String) -> String { return "" }
subscript(param: String) -> Int { return 1 }
subscript(param: Int) -> Int { return 1 }
subscript(param: Int) -> String { return "" }

// parameter label
func set(int: Int) {}
func set(string: String) {}

// concrete type from generic
func get(param: Array<String>) -> String { return "" }
func get(param: Array<Int>) -> Int { return 1 }

subscript(param: Array<String>) -> String { return "" }
subscript(param: Array<Int>) -> Int { return 1 }
}

When you specialize a generic type, like Array<Int>, you’re actually using a concrete type

Unfortunately, this does not work for NSObject subclass

Method ‘get()’ with Objective-C selector ‘get’ conflicts with previous declaration with the same Objective-C selector

1
2
3
4
5
class B: NSObject {

func get() -> String { return "" }
func get() -> Int { return 1 }
}

Generic function

We can overload generic functions as well

1
2
3
4
5
6
7
8
9
10
11
func f<T>(t: T) {
print("T")
}

func f(string: String) {
print("String")
}

func f(int: Int) {
print("Int")
}

How to check file under Library in macOS

Issue #200

1
2
3
let home = NSSearchPathForDirectoriesInDomains(.applicationScriptsDirectory, .userDomainMask, true).first!
let path = home.appending(".XcodeWayExtensions/XcodeWayScript.scpt")
let exists = FileManager.default.fileExists(atPath: path)

How to generate grpc protobuf files

Issue #197

protoc

https://grpc.io/docs/quickstart/go.html

Install the protoc compiler that is used to generate gRPC service code. The simplest way to do this is to download pre-compiled binaries for your platform(protoc--.zip) from here: https://github.com/google/protobuf/releases

Unzip this file.
Update the environment variable PATH to include the path to the protoc binary file.

Go protoc plugin

https://github.com/golang/protobuf

1
2
3
go get -u github.com/golang/protobuf/protoc-gen-go
export PATH=$PATH:$GOPATH/bin
source ~/.zshrc

Swift protoc plugin

https://github.com/grpc/grpc-swift

The recommended way to use Swift gRPC is to first define an API using the
Protocol Buffer
language and then use the
Protocol Buffer Compiler
and the Swift Protobuf
and Swift gRPC plugins to
generate the necessary support code.

1
2
3
4
git clone https://github.com/grpc/grpc-swift.git
cd grpc-swift
make
sudo cp protoc-gen-swift protoc-gen-swiftgrpc /usr/local/bin

Generate

1
protoc --swift_out=MyApp/Api --swiftgrpc_out=Client=true,Server=false:MyApp/Api --go_out=plugins=grpc:server/api api.proto

In case we need to cd

1
2
3
cd MyApp/Library/Models/Proto

protoc --swift_out=../Generated --swiftgrpc_out=Client=true,Server=false:../Generated api.proto

Empty

If remote import is needed, then the workaround is to download the that proto locally, for example empty.proto https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/empty.proto

Inside SwiftProtobuf pod, there is generated empty.pb.swift

1
2
3
4
5
6
7
8
9
public struct Google_Protobuf_Empty {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.

public var unknownFields = SwiftProtobuf.UnknownStorage()

public init() {}
}

To consume, we can

1
2
3
import SwiftProtobuf

let empty = Google_Protobuf_Empty()

oneof mode

1
2
3
4
5
6
message Person {
oneof mode {
Human human = 1;
Superman superman = 2;
}
}

Cannot convert value of type ‘Server_Person.OneOf_Mode’ to expected argument type ‘Server_Human’

Need to assign the mode

1
2
var person = Person()
person.mode = Person.OneOf_Mode.Human()

How to build a networking in Swift

Issue #195

Miami

Concerns

Parameter encoding is confusing

-https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#parameter-encoding

Query and body builder

HTTP

Lazy execution

Catch error

Implementation

Use Promise to handle chain and error

https://github.com/onmyway133/Miami/blob/master/Sources/Shared/Future/Future.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
import Foundation

public class Token {
private let lock = NSRecursiveLock()
private var _isCancelled = false
private var callbacks: [() -> Void] = []

public init() {}

public func isCancelled() -> Bool {
return lock.whenLock {
return _isCancelled
}
}

public func cancel() {
lock.whenLock {
guard self._isCancelled == false else {
return
}

self._isCancelled = true
self.callbacks.forEach { $0() }
self.callbacks.removeAll()
}
}

public func onCancel(_ callback: @escaping () -> Void) {
lock.whenLock {
self.callbacks.append(callback)
}
}
}

public class Resolver<T> {
public let queue: DispatchQueue
public let token: Token
private var callback: (Result<T, Error>) -> Void

public init(queue: DispatchQueue, token: Token, callback: @escaping (Result<T, Error>) -> Void) {
self.queue = queue
self.token = token
self.callback = callback
}

public func complete(value: T) {
self.handle(result: .success(value))
}

public func fail(error: Error) {
self.handle(result: .failure(error))
}

public func handle(result: Result<T, Error>) {
queue.async {
self.callback(result)
self.callback = { _ in }
}
}
}

public class Future<T> {
public let work: (Resolver<T>) -> Void

public init(work: @escaping (Resolver<T>) -> Void) {
self.work = work
}

public static func fail(error: Error) -> Future<T> {
return Future<T>.result(.failure(error))
}

public static func complete(value: T) -> Future<T> {
return .result(.success(value))
}

public static func result(_ result: Result<T, Error>) -> Future<T> {
return Future<T>(work: { resolver in
switch result {
case .success(let value):
resolver.complete(value: value)
case .failure(let error):
resolver.fail(error: error)
}
})
}

public func run(queue: DispatchQueue = .serial(), token: Token = Token(), completion: @escaping (Result<T, Error>) -> Void) {
queue.async {
if (token.isCancelled()) {
completion(.failure(NetworkError.cancelled))
return
}

let resolver = Resolver<T>(queue: queue, token: token, callback: completion)
self.work(resolver)
}
}

public func map<U>(transform: @escaping (T) -> U) -> Future<U> {
return Future<U>(work: { resolver in
self.run(queue: resolver.queue, token: resolver.token, completion: { result in
resolver.handle(result: result.map(transform))
})
})
}

public func flatMap<U>(transform: @escaping (T) -> Future<U>) -> Future<U> {
return Future<U>(work: { resolver in
self.run(queue: resolver.queue, token: resolver.token, completion: { result in
switch result {
case .success(let value):
let future = transform(value)
future.run(queue: resolver.queue, token: resolver.token, completion: { newResult in
resolver.handle(result: newResult)
})
case .failure(let error):
resolver.fail(error: error)
}
})
})
}

public func catchError(transform: @escaping (Error) -> Future<T>) -> Future<T> {
return Future<T>(work: { resolver in
self.run(queue: resolver.queue, token: resolver.token, completion: { result in
switch result {
case .success(let value):
resolver.complete(value: value)
case .failure(let error):
let future = transform(error)
future.run(queue: resolver.queue, token: resolver.token, completion: { newResult in
resolver.handle(result: newResult)
})
}
})
})
}

public func delay(seconds: TimeInterval) -> Future<T> {
return Future<T>(work: { resolver in
resolver.queue.asyncAfter(deadline: DispatchTime.now() + seconds, execute: {
self.run(queue: resolver.queue, token: resolver.token, completion: { result in
resolver.handle(result: result)
})
})
})
}

public func log(closure: @escaping (Result<T, Error>) -> Void) -> Future<T> {
return Future<T>(work: { resolver in
self.run(queue: resolver.queue, token: resolver.token, completion: { result in
closure(result)
resolver.handle(result: result)
})
})
}

public static func sequence(futures: [Future<T>]) -> Future<Sequence<T>> {
var index = 0
var values = [T]()

func runNext(resolver: Resolver<Sequence<T>>) {
guard index < futures.count else {
let sequence = Sequence(values: values)
resolver.complete(value: sequence)
return
}

let future = futures[index]
index += 1

future.run(queue: resolver.queue, token: resolver.token, completion: { result in
switch result {
case .success(let value):
values.append(value)
runNext(resolver: resolver)
case .failure(let error):
resolver.fail(error: error)
}
})
}

return Future<Sequence<T>>(work: runNext)
}
}

extension NSLocking {
@inline(__always)
func whenLock<T>(_ closure: () throws -> T) rethrows -> T {
lock()
defer { unlock() }
return try closure()
}

@inline(__always)
func whenLock(_ closure: () throws -> Void) rethrows {
lock()
defer { unlock() }
try closure()
}
}

Query builder to build query

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

public protocol QueryBuilder {
func build() -> [URLQueryItem]
}

public class DefaultQueryBuilder: QueryBuilder {
public let parameters: JSONDictionary

public init(parameters: JSONDictionary = [:]) {
self.parameters = parameters
}

public func build() -> [URLQueryItem] {
var components = URLComponents()

let parser = ParameterParser()
let pairs = parser
.parse(parameters: parameters)
.map({ $0 })
.sorted(by: <)

components.queryItems = pairs.map({ key, value in
URLQueryItem(name: key, value: value)
})

return components.queryItems ?? []
}

public func build(queryItems: [URLQueryItem]) -> String {
var components = URLComponents()
components.queryItems = queryItems.map({
return URLQueryItem(name: escape($0.name), value: escape($0.value ?? ""))
})

return components.query ?? ""
}

public func escape(_ string: String) -> String {
return string.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""
}
}

Body builder to build body

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

public class JsonBodyBuilder: BodyBuilder {
public let parameters: JSONDictionary

public init(parameters: JSONDictionary) {
self.parameters = parameters
}

public func build() -> ForBody? {
guard let data = try? JSONSerialization.data(
withJSONObject: parameters,
options: JSONSerialization.WritingOptions()
) else {
return nil
}

return ForBody(body: data, headers: [
Header.contentType.rawValue: "application/json"
])
}
}

Make request with networking

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

public class Networking {
public let session: URLSession
public let mockManager = MockManager()

public var before: (URLRequest) -> URLRequest = { $0 }
public var catchError: (Error) -> Future<Response> = { error in Future.fail(error: error) }
public var validate: (Response) -> Future<Response> = { Future.complete(value: $0) }
public var logResponse: (Result<Response, Error>) -> Void = { _ in }

public init(session: URLSession = .shared) {
self.session = session
}

public func make(options: Options, baseUrl: URL) -> Future<Response> {
let builder = UrlRequestBuilder()
do {
let request = try builder.build(options: options, baseUrl: baseUrl)
return make(request: request)
} catch {
return Future<Response>.fail(error: error)
}
}

public func make(request: URLRequest) -> Future<Response> {
if let mock = mockManager.findMock(request: request) {
return mock.future.map(transform: { Response(data: $0, urlResponse: URLResponse()) })
}

let future = Future<Response>(work: { resolver in
let task = self.session.dataTask(with: request, completionHandler: { data, response, error in
if let data = data, let urlResponse = response {
resolver.complete(value: Response(data: data, urlResponse: urlResponse))
} else if let error = error {
resolver.fail(error: NetworkError.urlSession(error, response))
} else {
resolver.fail(error: NetworkError.unknownError)
}
})

resolver.token.onCancel {
task.cancel()
}

task.resume()
})

return future
.catchError(transform: self.catchError)
.flatMap(transform: self.validate)
.log(closure: self.logResponse)
}
}

Mock a 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
import Foundation

public class Mock {
public let options: Options
public let future: Future<Data>

public init(options: Options, future: Future<Data>) {
self.options = options
self.future = future
}

public static func on(options: Options, data: Data) -> Mock {
return Mock(options: options, future: Future.complete(value: data))
}

public static func on(options: Options, error: Error) -> Mock {
return Mock(options: options, future: Future.fail(error: error))
}

public static func on(options: Options, file: String, fileExtension: String, bundle: Bundle = Bundle.main) -> Mock {
guard
let url = bundle.url(forResource: file, withExtension: fileExtension),
let data = try? Data(contentsOf: url)
else {
return .on(options: options, error: NetworkError.invalidMock)
}

return .on(options: options, data: data)
}
}

How to construct URL with URLComponents and appendPathComponent in Swift

Issue #193

1
2
3
var components = URLComponents(string: "https://google.com/")
components?.path = "abc/"
components?.url

-> nil

1
2
3
4
var components = URLComponents(string: "https://google.com/")
components?.path = "/abc/"
components?.url
components?.queryItems = [URLQueryItem(name: "q", value: "pokemon")]

-> https://google.com/abc/?q=pokemon

1
2
3
var url = URL(string: "https://google.com/")
url?.appendPathComponent("/abc?q=pokemon")
url

-> https://google.com//abc%3Fq=pokemon

Curry in Swift and Javascript

Issue #185

You may encounter curry in everyday code without knowing it. Here is a bit of my reflections on curry and how to apply it in Javascript and Swift.

Taking one parameter in Haskell

In Haskell, all function officially takes only 1 parameter. Function with many parameters are just curried, which means they will be partially applied. Calling sum 1 just returns a function with 1 parameter, then 2is passed into this function. The following 2 function calls are the same.

ghci> sum 1 2
3 
ghci> (max 1) 2
3

I tend to think of curried function or partially applied function as something that carry dependencies at each application step. Each curried function can be assigned to variable or pass around, or as returned value.

Curry in Swift for predicate

When I was trying to make my own Signal library, I have

and Event

filter

Then there should be a filter for Signal. The idea of filter is that we should update signal if the Event is Next with right filtered value

public func filter(f: T -> Bool) -> Signal<T>{
    let signal = Signal<T>()
    subscribe { result in
        switch(result) {
        case let .Success(value):
            if f(value) {
                signal.update(result)
            }
        case let .Error(error): signal.update(.Error(error))
        }
    }
    return signal
}

2 parameters

But having Event as another monad, I think it should be more encapsulated if that switching logic gets moved into the Event. Here the filter takes 2 params

Event.swift

func filter(f: T -> Bool, callback: (Event<T> -> Void)) {
        switch self {
        case let .Next(value) where f(value):
            callback(self)
        case .Failed:
            callback(self)
        default:
            break
    }
}

Signal.swift

public func filter(f: T -> Bool) -> Signal<T> {
    let signal = Signal<T>()

    subscribe { event in
        event.filter(f, callback: signal.update)
    }

    return signal
}

Currying

With currying, we can make filter a more abstract function, and defer the decision to pass the callback param. It is a little carried away but I find it helpful this way

Now filter accepts 1 param, and it returns a function that takes callback as its param

Event.swift

func filter(f: T -> Bool) -> ((Event<T> -> Void) -> Void) {
        return { g in
            switch self {
            case let .Next(value) where f(value):
                g(self)
            case .Failed:
                g(self)
            default:
                break
            }
        }
    }

Signal.swift

public func filter(f: T -> Bool) -> Signal<T> {
        let signal = Signal<T>()

        subscribe { event in
            event.filter(f)(signal.update)
        }

        return signal
    }

Curry syntax in Swift 2 and above

Swift 2 supports curry syntax function

func sum(a: Int)(b: Int) -> Int {
    return a + b
}

let sumWith5 = sum(5)
let result = sumWith5(b: 10)

Unfortunately, the syntactic sugar for declaring curry has been dropped since Swift 3. You may want to find out in Bidding farewell to currying. But it’s not a big deal as we can easily create curry function. It is just a function that returns another function.

Using curry for partial application in UIKit

I used this curry technique in my Xkcd app. See MainController.swift. MainController is vanilla UITabBarController with ComicsController and FavoriteController , all embedded in UINavigationViewController .

The feature is that when a comic is selected, a comic detail screen should be pushed on top of the navigation stack. For example in ComicsController

/// Called when a comic is selected  
var selectComic: ((Comic) -> Void)?

All ComicsController needs to know is to call that selectComic closure with the chosen Comic, and someone should know how to handle that selection. Back to the handleFlow function inside MainController.

private func handleFlow() {
  typealias Function = (UINavigationController) -> (Comic) -> Void
  let selectComic: Function = { [weak self] navigationController in
    return { (comic: Comic) in
      guard let self = self else {
        return
      }

  let detailController = self.makeDetail(comic: comic)
      navigationController.pushViewController(detailController, animated: true)
    }
  }

  comicsController.selectComic = selectComic(comicNavigationController)
  favoriteController.selectComic = selectComic(favoriteNavigationController)
}

I declared Function as typealias to explicitly state the curry function that we are going to build

typealias Function = (UINavigationController) -> (Comic) -> Void

We build selectComic as curried function, that takes UINavigationViewController and returns a function that takes Comic and returns Void . This way when we partially apply selectComic with the a navigationController , we get another function that has navigationController as dependency, and ready to be assigned to selectComic property in comicsController .

Curry promised function in Javascript

I like to work with Promise and async/await in Javascript. It allows chainable style and easy to reason about. So when working with callbacks in Javascript, for example callback from native modules in React Native, I tend to convert them into Promise.

For example when working with HealthKit, we need to expose a native modules around it

// [@flow](http://twitter.com/flow)

import { NativeModules } from 'react-native'

type HealthManagerType = {
  checkAuthorisation: ((string) => void)) => void,
  authorise: ((boolean) => void)) => void,
  readWorkout: (Date, Date, () => void)) => void,
  readDailySummary: (Date, Date, () => void)) => void,
  readMeasurement: (Date, Date, () => void)) => void
}

const HealthManager: HealthManagerType = NativeModules.HealthManager
export default HealthManager

We can build a toPromise function that can convert a function with callback into Promise

// [@flow](http://twitter.com/flow)

const toPromise = (f: (any) => void) => {
  return new Promise<any>((resolve, reject) => {
    try {
      f((result) => {
        resolve(result)
      })
    } catch (e) {
      reject(e)
    }
  })
}

export default toPromise

However, as you can see in the signature, it only works with a callback of type (any) => void In other words, this callback must have exactly 1 parameter, because a Promise can either returns a value or throws an error.

To remedy this, we can build a curry function that can turns function with either 1, 2, 3 parameters into curried function. Thanks to the dynamic nature of Javascript, we have

// [@flow](http://twitter.com/flow)

function curry0(f: () => void) {
  return f()
}

function curry1(f: (any) => void) {
  return (p1: any) => {
    return f(p1)
  }
}

function curry2(f: (any, any) => void) {
  return (p1: any) => {
    return (p2: any) => {
      return f(p1, p2)
    }
  }
}

function curry3(f: (any, any, any) => void) {
  return (p1: any) => {
    return (p2: any) => {
      return (p3: any) => {
        return f(p1, p2, p3)
      }
    }
  }
}

export default {
  curry0,
  curry1,
  curry2,
  curry3
}

So with a function that have 3 parameters, we can use curry3 to partially apply the first 2 parameters. Then we have a function that accepts just a callback, and this is turned into Promise via toPromise

const readWorkout = curry.curry3(HealthManager.readWorkout)(DateUtils.startDate))(DateUtils.endDate))

const workouts = await toPromise(readWorkout)

Where to go from here

Here are some of my favorite posts to read more about curry

How to run executable in macOS

Issue #176

Enable executable

1
chmod +x executable

Add executable file to target
Use Process with correct launchPad

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

protocol TaskDelegate: class {
func task(task: Task, didOutput string: String)
func taskDidComplete(task: Task)
}

class Task {

weak var delegate: TaskDelegate?
let process = Process()

func run(arguments: [String]) {
DispatchQueue.background.async {
let launchPath = Bundle.main.path(forResource: "executable", ofType: "")!
self.run(launchPath: launchPath, arguments: arguments)
}
}

func stop() {
DispatchQueue.background.async {
if self.process.isRunning {
self.process.terminate()
}
}
}

// MARK: - Helper

private func run(launchPath: String, arguments: [String]) {
let process = Process()
process.launchPath = launchPath
process.arguments = arguments

let stdOut = Pipe()
process.standardOutput = stdOut
let stdErr = Pipe()
process.standardError = stdErr

let handler = { [weak self] (file: FileHandle!) -> Void in
let data = file.availableData
guard let output = NSString(data: data, encoding: String.Encoding.utf8.rawValue) else {
return
}

guard let strongSelf = self,
let string = output.components(separatedBy: "\n").first else {
return
}

DispatchQueue.main.async {
strongSelf.delegate?.task(task: strongSelf, didOutput: string)
}
}

stdErr.fileHandleForReading.readabilityHandler = handler
stdOut.fileHandleForReading.readabilityHandler = handler

process.terminationHandler = { [weak self] (task: Process?) -> () in
stdErr.fileHandleForReading.readabilityHandler = nil
stdOut.fileHandleForReading.readabilityHandler = nil

guard let strongSelf = self else {
return
}

DispatchQueue.main.async {
strongSelf.delegate?.taskDidComplete(task: strongSelf)
}
}

process.launch()
process.waitUntilExit()
}
}

How to make scrollable vertical NSStackView

Issue #173

You might need to flip NSClipView

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

final class ScrollableStackView: NSView {
final class FlippedClipView: NSClipView {
override var isFlipped: Bool {
return true
}
}

override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
setup()
}

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

let stackView: NSStackView = withObject(NSStackView()) {
$0.orientation = .vertical
}

private let scrollView: NSScrollView = NSScrollView()

private func setup() {
addSubview(scrollView)
scrollView.hasVerticalScroller = true
scrollView.drawsBackground = false

activate(
scrollView.anchor.edges
)

let clipView = FlippedClipView()
clipView.translatesAutoresizingMaskIntoConstraints = false

clipView.drawsBackground = false
scrollView.contentView = clipView

activate(
clipView.anchor.edges.equal.to(scrollView.anchor)
)

scrollView.documentView = stackView
stackView.translatesAutoresizingMaskIntoConstraints = false
activate(
clipView.anchor.left.top.right.equal.to(stackView.anchor)
)
}
}

Read more

How to load top level view from xib in macOS

Issue #171

1
2
3
var views: NSArray?
NSNib(nibNamed: NSNib.Name("ProfileView"), bundle: nil)?.instantiate(withOwner: nil, topLevelObjects: &views)
let profileView = views!.compactMap({ $0 as? ProfileView }).first!

UITableViewCell and Model

Issue #154

The most common UI element in iOS is UITableView, and the most common task is to display the UITableViewCell using the model.

Although the title specifies UITableViewCell, but the problem involves other views (UICollectionView, custom view, …) as well

There are many debates about this, so here I want to make a summary, all in my opinion

It started with

UITableViewCell Is Not a Controller
This article follows strict MVC and states we should not pass model object to cell and let cell manipulate directly on the model. He shows several examples that point out this job is not suitable for the cell. Instead this job should be done on the Controller (UIViewController, UITableViewDataSource, …)

Category for simple case

Skinnier Controllers Using View Categories
This article states that we should keep ViewController skinnier by transfering the job (mapping model object to cell) to the cell category

Using subclassing

UITableViewCell Is Not a Controller, But…
This articles explains the beauty of subclassing to take advantage of Polymorphism. In the theming example he gives, we see that Controller ‘s job now is to select the correct cell subclass, and the subclass ‘s job is to know how to use the model
“When a UITableViewCell subclass accepts a model object parameter and updates its constituent subviews as I have described, it is behaving as a data transformer, not a controller”

Model Presenter

Model View Controller Presenter
This article shows that using subclassing and category will have duplication implementation when you have more cells and models. After several optimizations, he finally gets to the Model Presenter, which is the center object who knows how to represent the model in different view.
“This is an object that knows how to represent every aspect of a certain model”

MVVM

MVC, MVVM, FRP, And Building Bridges
This article explains MVVM but it touches our problem directly. The problem with cell and model actually is

  1. How to map the model to the cell
  2. Who will do this job? The cell, Controller or another Model Mapping object ?

The ViewModel is actually who does this work, which is to transform the model to something the view can easily use

“A table view data source is none of these things. It’s purely a layer between the table view and the model. The model defines lists of things, but the table view data source transform those lists into sections and rows. It also returns the actual table view cells, but that’s not what I’m focusing on here. The key is its role as a middle-tier data transformer.”

Do we access the cell ‘s subviews

Paul on UITableViewCell
Brent follows with another post explaining how cell ‘s subviews should not be accessed outside of the cell

Data Source

Clean table view code
This article deals with Bridging the Gap Between Model Objects and Cells.
“At some point we have to hand over the data we want to display into the view layer. Since we still want to maintain a clear separation between the model and the view, we often offload this task to the table view’s data source. This kind of code clutters the data source with specific knowledge about the design of the cell. We are better off factoring this out into a category of the cell class”

This together with Lighter View Controllers shows a practical example that deals with most cases

Reference

  1. Should UITableViewCells Know About Model Objects?

Learning from Open Source Generic Factory

Issue #148

From https://github.com/devxoul/Pure/blob/master/Sources/Pure/FactoryModule.swift

1
2
3
4
5
6
7
8
public protocol FactoryModule: Module {

/// A factory for `Self`.
associatedtype Factory = Pure.Factory<Self>

/// Creates an instance of a module with a dependency and a payload.
init(dependency: Dependency, payload: Payload)
}

From https://github.com/devxoul/Pure/blob/master/Sources/Pure/Factory.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
open class Factory<Module: FactoryModule> {
private let dependencyClosure: () -> Module.Dependency

/// A static dependency of a module.
open var dependency: Module.Dependency {
return self.dependencyClosure()
}

/// Creates an instance of `Factory`.
///
/// - parameter dependency: A static dependency which should be resolved in a composition root.
public init(dependency: @autoclosure @escaping () -> Module.Dependency) {
self.dependencyClosure = dependency
}

/// Creates an instance of a module with a runtime parameter.
///
/// - parameter payload: A runtime parameter which is required to construct a module.
open func create(payload: Module.Payload) -> Module {
return Module.init(dependency: self.dependency, payload: payload)
}
}

From https://github.com/devxoul/Pure/blob/master/Tests/PureTests/PureSpec.swift#L72

1
2
3
4
5
6
let factory = FactoryFixture<Dependency, Payload>.Factory(dependency: .init(
networking: "Networking A"
))
let instance = factory.create(payload: .init(id: 100))
expect(instance.dependency.networking) == "Networking A"
expect(instance.payload.id) == 100

Using camelCase for abbreviations

Issue #147

Each language and platform has its own coding style guide. This goes true when it comes to abbreviations. I’ve had some debates about whether to use JSON or Json, URL or Url, HTTP or Http.

I personally prefer camelCase, so I’m very happy to see that Kotlin is on my side. See Kotlin Style guide, I think this guide should be applied in other languages, such as Swift 😛

Sometimes there is more than one reasonable way to convert an English phrase into camel case, such as when acronyms or unusual constructs like “IPv6” or “iOS” are present. To improve predictability, use the following scheme.

Beginning with the prose form of the name:

  1. Convert the phrase to plain ASCII and remove any apostrophes. For example, “Müller’s algorithm” might become “Muellers algorithm”.
  1. Divide this result into words, splitting on spaces and any remaining punctuation (typically hyphens).

Recommended: if any word already has a conventional camel-case appearance in common usage, split this into its constituent parts (e.g., “AdWords” becomes “ad words”). Note that a word such as “iOS” is not really in camel case per se; it defies any convention, so this recommendation does not apply.

  1. Now lowercase everything (including acronyms), then uppercase only the first character of:

…each word, to yield pascal case, or

…each word except the first, to yield camel case

  1. Finally, join all the words into a single identifier.

Note that the casing of the original words is almost entirely disregarded.

Prose form Correct Incorrect
“XML Http Request” XmlHttpRequest XMLHTTPRequest
“new customer ID” newCustomerId newCustomerID
“inner stopwatch” innerStopwatch innerStopWatch
“supports IPv6 on iOS” supportsIpv6OnIos supportsIPv6OnIOS
“YouTube importer” YouTubeImporterYoutubeImporter*

About iOS or IOS, I think I would go with IOS. I think React Native thinks so too

NavigatorIOS looks and feels just like UINavigationController, because it is actually built on top of it.

Read more

How to use standalone UINavigationBar in iOS

Issue #144

There are times we want the same UIViewController to look good when it’s presented modally or pushed from UINavigationController stack. Take a look at BarcodeScanner and the PR https://github.com/hyperoslo/BarcodeScanner/pull/82

When it is presented, we need a header view so that we can show a title and a close button. We can create a custom HeaderView that inherits from UIView or either embed it in a UINavigationController before presenting.

If we go with the controller being embedded in UINavigationController approach, it will collide with the other UINavigationController when it is pushed.

If we go with custom HeaderView, then we need to layout the view so that it looks good on both portrait and landscape, and on iPhone X that as safeAreaLayoutGuide.

Using standalone UINavigationBar

Since UINavigationController uses UINavigationBar under the hood, which uses UINavigationItem info to present the content. We can imitate this behavior by using a standalone UINavigationBar. See Adding Content to a Standalone Navigation Bar

In the vast majority of scenarios you will use a navigation bar as part of a navigation controller. However, there are situations for which you might want to use the navigation bar UI and implement your own approach to content navigation. In these situations, you can use a standalone navigation bar.

A navigation bar manages a stack of UINavigationItem objects

The beauty is that our standalone UINavigationBar and that of UINavigationController are the same, use the same UINavigationItem and no manual layout are needed

Declare UINavigationItem

We can just set properties like we did with a normal navigationItem

1
2
3
let standaloneItem = UINavigationItem()
standaloneItem.leftBarButtonItem = UIBarButtonItem(customView: closeButton)
standaloneItem.titleView = UILabel()

Adding UINavigationBar

Customise your bar, then declare layout constraints. You only need to pin left, right, and top. Note that you need to implement UINavigationBarDelegate to attach bar to status bar, so that it appears good on iPhone X too

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let navigationBar = UINavigationBar()
navigationBar.isTranslucent = false
navigationBar.delegate = self
navigationBar.backgroundColor = .white
navigationBar.items = [standaloneItem]

navigationBar.translatesAutoresizingMaskIntoConstraints = false
navigationBar.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
navigationBar.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true

if #available(iOS 11, *) {
navigationBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
} else {
navigationBar.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
}
1
2
3
4
5
extension BarcodeScannerController: UINavigationBarDelegate {
public func position(for bar: UIBarPositioning) -> UIBarPosition {
return .topAttached
}
}

Inside UINavigationController

When this UIViewController is pushed from a UINavigationController stack, we just need to hide our standalone navigationBar. If we prefer the default back button, we don’t need to set leftBarButtonItem

On iOS 10, you need to call sizeToFit for any items in UINavigationItem for it to get actual size

1
2
3
4
5
6
7
8
9
10
11
12
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

if navigationController != nil {
let label = HeaderElement.makeLabel()
label.sizeToFit()
navigationItem.titleView = label
navigationBar.isHidden = true
} else {
navigationBar.isHidden = false
}
}

How to generate QR code in AppKit

Issue #140

I need to generate QR code in https://github.com/onmyway133/AddressGenerator. Fortunately with CoreImage filter, it is very easy. Code is in Swift 4

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

final class QRCodeGenerator {
func generate(string: String, size: CGSize) -> NSImage? {
guard let data = string.data(using: .utf8) else {
return nil
}

// Filter
guard let filter = CIFilter(name: "CIQRCodeGenerator") else {
return nil
}

filter.setValue(data, forKey: "inputMessage")
filter.setValue("Q", forKey: "inputCorrectionLevel")

// CIImage
guard let ciImage = filter.outputImage else {
return nil
}

// NSImage
let rep = NSCIImageRep(ciImage: ciImage)
let image = NSImage(size: rep.size)
image.addRepresentation(rep)

// Scale
let finalImage = NSImage(size: size)
finalImage.lockFocus()
NSGraphicsContext.current?.imageInterpolation = .none
image.draw(in: NSRect(origin: .zero, size: size))
finalImage.unlockFocus()

return finalImage
}
}

Hiding back button in navigation bar in iOS

Issue #137

Use a custom NavigationController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import UIKit

class NavigationController: UINavigationController {
override func viewDidLoad() {
super.viewDidLoad()
navigationBar.tintColor = .white
navigationBar.barStyle = .black
navigationBar.isTranslucent = false
}

override func pushViewController(_ viewController: UIViewController, animated: Bool) {
viewController.navigationItem.backBarButtonItem =
UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
super.pushViewController(viewController, animated: animated)
}
}

How to make NSCollectionView programatically in Swift

Issue #131

Here’s how to create NSCollectionView programatically. We need to embed it inside NScrollView for scrolling to work. Code is in Swift 4

NSCollectionView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let layout = NSCollectionViewFlowLayout()
layout.minimumLineSpacing = 4

collectionView = NSCollectionView()
collectionView.dataSource = self
collectionView.delegate = self
collectionView.collectionViewLayout = layout
collectionView.allowsMultipleSelection = false
collectionView.backgroundColors = [.clear]
collectionView.isSelectable = true
collectionView.register(
Cell.self,
forItemWithIdentifier: NSUserInterfaceItemIdentifier(rawValue: "Cell")
)

NScrollView

1
2
3
scrollView = NSScrollView()
scrollView.documentView = collectionView
view.addSubview(scrollView)

NSCollectionViewItem

1
2
3
4
5
6
7
8
9
final class Cell: NSCollectionViewItem {
let label = Label()
let myImageView = NSImageView()

override func loadView() {
self.view = NSView()
self.view.wantsLayer = true
}
}

NSCollectionViewDataSource

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int {
return coins.count
}

func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
let cell = collectionView.makeItem(
withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "Cell"),
for: indexPath
) as! Cell

let coin = coins[indexPath.item]

cell.label.stringValue = coin.name
cell.coinImageView.image =
NSImage(named: NSImage.Name(rawValue: "USD"))
?? NSImage(named: NSImage.Name(rawValue: "Others"))

return cell
}

NSCollectionViewDelegateFlowLayout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func collectionView(_ collectionView: NSCollectionView, didSelectItemsAt indexPaths: Set<IndexPath>) {
guard let indexPath = indexPaths.first,
let cell = collectionView.item(at: indexPath) as? Cell else {
return
}
}

func collectionView(_ collectionView: NSCollectionView, didDeselectItemsAt indexPaths: Set<IndexPath>) {
guard let indexPath = indexPaths.first,
let cell = collectionView.item(at: indexPath) as? Cell else {
return
}
}

func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> NSSize {

return NSSize(
width: collectionView.frame.size.width,
height: 40
)
}

Updated at 2020-05-22 11:48:15