How to use AnyHashable in Swift

Issue #453

From documentation

A type-erased hashable value.

DiscussionThe AnyHashable type forwards equality comparisons and hashing operations to an underlying hashable value, hiding its specific underlying type.You can store mixed-type keys in dictionaries and other collections that require Hashable conformance by wrapping mixed-type keys in AnyHashable instances

1
2
3
4
5
6
7
8
9
10
let descriptions: [AnyHashable: Any] = [
AnyHashable("😄"): "emoji",
AnyHashable(42): "an Int",
AnyHashable(Int8(43)): "an Int8",
AnyHashable(Set(["a", "b"])): "a set of strings"
]
print(descriptions[AnyHashable(42)]!) // prints "an Int"
print(descriptions[AnyHashable(43)]) // prints "nil"
print(descriptions[AnyHashable(Int8(43))]!) // prints "an Int8"
print(descriptions[AnyHashable(Set(["a", "b"]))]!) // prints "a set of strings"

We don’t necessarily need to map from [AnyHashable: Any] to [String: Any], we can just access via string key

1
userInfo["aps"]

How to register for alert push notification in iOS

Issue #452

Use UserNotifications framework

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 FirebaseMessaging
import UserNotifications

final class PushHandler: NSObject {
private let center = UNUserNotificationCenter.current()
private let options: UNAuthorizationOptions = [.alert]

func setup() {
Messaging.messaging().delegate = self
}

func register() {
center.requestAuthorization(options: options, completionHandler: { granted, error in
print(granted, error)
self.getSettings()
})
}

func getSettings() {
center.getNotificationSettings(completionHandler: { settings in
guard
case let .authorized = settings.authorizationStatus,
case let .enabled = settings.alertSetting,
settings.alertStyle != .none
else {
return
}

// TODO
})
}

func handle(userInfo: [AnyHashable: Any]) {

}
}

extension PushHandler: MessagingDelegate {
func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String) {
print(fcmToken)
}

func messaging(_ messaging: Messaging, didReceive remoteMessage: MessagingRemoteMessage) {
print(remoteMessage)
}
}
1
2
3
4
5
6
7
8
9
10
11
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
completionHandler(.noData)

Deps.pushHandler.handle(userInfo: userInfo)
}
}

A test message from Firebase looks like

1
2
3
4
5
6
[AnyHashable("google.c.a.e"): 1, AnyHashable("google.c.a.ts"): 1570701857, AnyHashable("aps"): {
alert = {
body = Test;
title = Test;
};
}, AnyHashable("google.c.a.udt"): 0, AnyHashable("google.c.a.c_l"): Test, AnyHashable("gcm.message_id"): 1570701857965182, AnyHashable("google.c.a.c_id"): 1257698497812622975, AnyHashable("gcm.n.e"): 1]

How to flat map publisher of publishers in Combine

Issue #451

For some services, we need to deal with separated APIs for getting ids and getting detail based on id.

To chain requests, we can use flatMap and Sequence, then collect to wait and get all elements in a single publish

FlatMap

Transforms all elements from an upstream publisher into a new or existing publisher.

1
struct FlatMap<NewPublisher, Upstream> where NewPublisher : Publisher, Upstream : Publisher, NewPublisher.Failure == Upstream.Failure

Publishers.Sequence

A publisher that publishes a given sequence of elements.

1
struct Sequence<Elements, Failure> where Elements : Sequence, Failure : Error
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
func fetchItems(completion: @escaping ([ItemProtocol]) -> Void) {
requestCancellable = URLSession.shared
.dataTaskPublisher(for: topStoriesUrl())
.map({ $0.data })
.decode(type: [Int].self, decoder: JSONDecoder())
.flatMap({ (ids: [Int]) -> AnyPublisher<[HackerNews.Item], Error> in
let publishers = ids.prefix(10).map({ id in
return URLSession.shared
.dataTaskPublisher(for: self.storyUrl(id: id))
.map({ $0.data })
.decode(type: HackerNews.Item.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
})

return Publishers.Sequence<[AnyPublisher<HackerNews.Item, Error>], Error>(sequence: publishers)
// Publishers.Sequence<[AnyPublisher<HackerNews.Item, Error>], Error>
.flatMap({ $0 })
// Publishers.FlatMap<AnyPublisher<HackerNews.Item, Error>, Publishers.Sequence<[AnyPublisher<HackerNews.Item, Error>], Error>>
.collect()
// Publishers.Collect<Publishers.FlatMap<AnyPublisher<HackerNews.Item, Error>, Publishers.Sequence<[AnyPublisher<HackerNews.Item, Error>], Error>>>
.eraseToAnyPublisher()
// AnyPublisher<[HackerNews.Item], Error>
})
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
.sink(receiveCompletion: { completionStatus in
switch completionStatus {
case .finished:
break
case .failure(let error):
print(error)
}
}, receiveValue: { items in
completion(items)
})
}

How to make container view in SwiftUI

Issue #450

Following the signatures of ScrollView and Group, we can create our own container

1
2
3
4
5
6
public struct ScrollView<Content> : View where Content : View {

/// The content of the scroll view.
public var content: Content

}
1
2
3
4
5
6
7
8
9
10
extension Group : View where Content : View {

/// The type of view representing the body of this view.
///
/// When you create a custom view, Swift infers this type from your
/// implementation of the required `body` property.
public typealias Body = Never

@inlinable public init(@ViewBuilder content: () -> Content)
}

For example, below is a FullWidth that encapsulate a child element and make it full width

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 SwiftUI

struct FullWidth<Content>: View where Content: View {
let content: Content

var body: some View {
GeometryReader { geometry in
self.content
.frame(width: geometry.size.width, height: geometry.size.width, alignment: .center)
.padding(.bottom, geometry.size.width)
}
}

@inlinable public init(@ViewBuilder content: () -> Content) {
self.content = content()
}
}

struct FullWidth_Previews: PreviewProvider {
static var previews: some View {
FullWidth {
Text("")
}
}
}

One problem with GeometryReader is that the size of the children can’t affect the size of its container. We need to apply Geometry at the root level

1
2
3
4
5
6
7
8
9
10
11
12
var body: some View {
GeometryReader { geometry in
ScrollView {
VStack(alignment: .leading, spacing: 8) {
Text("title")
RemoteImage(url: self.item.url)
.frame(width: geometry.size.width, height: geometry.size.height, alignment: .center)
Text("footer")
}
}
}
}

How to show web content as QR code in SwiftUI in watchOS

Issue #449

WatchKit does not have Web component, despite the fact that we can view web content

A workaround is to show url as QR code

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

struct QRCodeView: View {
let title: String
let url: URL

var body: some View {
GeometryReader { geometry in
VStack {
self.makeImage(size: geometry.size)
.padding(.top, 10)
Text("Scan to open")
.font(.system(.footnote))
}.navigationBarTitle(self.title)
}
}

private func makeImage(size: CGSize) -> some View {
let value = size.height - 30
return RemoteImage(url: self.url)
.frame(width: value, height: value, alignment: .center)
}
}

How to load remote image in SwiftUI

Issue #448

Use ObservableObject and onReceive to receive event. URLSession.dataTask reports in background queue, so need to .receive(on: RunLoop.main) to receive events on main queue.

For better dependency injection, need to use ImageLoader from Environment

There should be a way to propagate event from Publisher to another Publisher, for now we use sink

ImageLoader.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
import Combine
import WatchKit

class ImageLoader: ObservableObject {
private var cancellable: AnyCancellable?
let objectWillChange = PassthroughSubject<UIImage?, Never>()

func load(url: URL) {
self.cancellable = URLSession.shared
.dataTaskPublisher(for: url)
.map({ $0.data })
.eraseToAnyPublisher()
.receive(on: RunLoop.main)
.map({ UIImage(data: $0) })
.replaceError(with: nil)
.sink(receiveValue: { image in
self.objectWillChange.send(image)
})
}

func cancel() {
cancellable?.cancel()
}
}

RemoteImage.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
import SwiftUI
import WatchKit

struct RemoteImage: View {
let url: URL
let imageLoader = ImageLoader()
@State var image: UIImage? = nil

var body: some View {
Group {
makeContent()
}
.onReceive(imageLoader.objectWillChange, perform: { image in
self.image = image
})
.onAppear(perform: {
self.imageLoader.load(url: self.url)
})
.onDisappear(perform: {
self.imageLoader.cancel()
})
}

private func makeContent() -> some View {
if let image = image {
return AnyView(
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
)
} else {
return AnyView(Text("😢"))
}
}
}

How to do navigation in SwiftUI in watchOS

Issue #447

NavigationView is not available on WatchKit, but we can just use NavigationLink

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
List(services.map({ AnyService($0) })) { anyService in
NavigationLink(destination:
ItemsView(service: anyService.service)
.navigationBarTitle(anyService.service.name)
.onDisappear(perform: {
anyService.service.requestCancellable?.cancel()
})
) {
HStack {
Image(anyService.service.name)
.resizable()
.frame(width: 30, height: 30, alignment: .leading)
Text(anyService.service.name)
}
}
}

Adding NavigationLink to a View adds a round shadow cropping effect, which is usually not want we want.

But we shouldn’t wrap Button as Button handles its own touch event, plus it has double shadow effect.

1
2
3
4
5
6
7
NavigationLink(destination:
QRCodeView(title: item.title, url: item.url)
) {
Button(action: {}) {
Text("Open")
}
}

Just use Text and it’s good to go

1
2
3
4
5
NavigationLink(destination:
QRCodeView(title: item.title, url: item.url)
) {
Text("Open")
}

How to use protocol in List in SwiftUI

Issue #446

Suppose we have Service protocol, and want to use in List

1
2
3
protocol Service {
var name: String { get }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct MainView: View {
let services = [
Car()
Plane()
]

var body: some View {
List(services) { service in
HStack {
Image(service.name)
Text(service.name)
}
}
}
}

This is not possible because item in List needs to conform to Identifiable

Protocol type ‘Service’ cannot conform to ‘Identifiable’ because only concrete types can conform to protocols

Type eraser

In the same way that SwiftUI uses type eraser, for example AnyView, we can introduce AnyService to work around this

1
2
3
4
5
6
7
var body: some View {
if useImage {
return AnyView(Image("my image"))
} else {
return AnyView(Text("my text"))
}
}

Make AnyService conform to Identifiable

1
2
3
4
5
6
7
8
9
struct AnyService: Identifiable {
let id: String
let service: Service

init(_ service: Service) {
self.service = service
self.id = service.name
}
}

Then in our View, we just need to declare services wrapped inside AnyService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct MainView: View {
let services = [
AnyService(Car()),
AnyService(Plane())
]

var body: some View {
List(services) { anyService in
HStack {
Image(anyService.service.name)
Text(anyService.service.name)
}
}
}
}

A bit refactoring, we can just declare normal services and map them

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct MainView: View {
let services: [Service] = [
Car(),
Plane()
]

var body: some View {
List(services.map({ AnyService($0 })) { anyService in
HStack {
Image(anyService.service.name)
Text(anyService.service.name)
}
}
}
}

How to support Swift Package Manager for existing projects

Issue #445

How to add SPM

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
// swift-tools-version:5.1

import PackageDescription

let package = Package(
name: "Anchors",
platforms: [
.macOS(.v10_11),
.iOS(.v9),
.tvOS(.v9)
],
products: [
.library(
name: "Anchors",
targets: ["Anchors"]),
],
targets: [
.target(
name: "Anchors",
dependencies: [],
path: "Sources"
),
.testTarget(
name: "AnchorsTests",
dependencies: ["Anchors"]),
],
swiftLanguageVersions: [.v5]
)

To test, swift test to test locally, this should validate Package.swift too

Support multiple platform

To support multiple platform, use platform check

1
2
3
#if canImport(UIKit)
import UIKit
#endif

Use sources in Target

If this property is nil, all valid source files in the target’s path will be included and specified paths are relative to the target path.

A path can be a path to a directory or an individual source file. In case of a directory, the Swift Package Manager searches for valid source files recursively inside it.

1
2
3
4
5
6
.target(
name: "EasyClosureiOS",
dependencies: [],
path: "Sources",
sources: ["Shared", "iOS"]
)

Error

error: target ‘EasyClosuremacOS’ has sources overlapping sources

How to use

  • SPM packages are at DerivedData/MyApp/SourcePackages/checkouts
  • Delete DerivedData for Xcode to fetch new contents from GitHub
  • Clean build folder as Xcode often caches changes from frameworks

Read more

How to fix unreadable ASCII characters in Swift

Issue #444

To avoid compiler error Unprintable ASCII character found in source file, from Swift 5, we can use isASCII.

Run this from the generator app that generates Swift code.

1
let normalized = weirdString.filter({ $0.isASCII })

For more check, see Character https://developer.apple.com/documentation/swift/character/3127011-isletter

Watch out for

  • Delete/House symbol ⌂ code 127 (0x7f)

The above does not seem to work, use find to find  character (copy Sublime Text to see 0x7f character)

1
find ./ -type f -exec sed -i '' -e 's///' {} \;

How to style NSTextView and NSTextField in macOS

Issue #443

1
2
let textField = NSTextField()
textField.focusRingType = .none
1
2
3
4
5
6
7
8
let textView = NSTextView()
textView.insertionPointColor = R.color.caret
textView.isRichText = false
textView.importsGraphics = false
textView.isEditable = true
textView.isSelectable = true
textView.drawsBackground = false
textView.allowsUndo = true

How to center NSWindow in screen

Issue #442

On macOS, coordinate origin is bottom left

1
2
3
4
5
let window = NSWindow(contentRect: rect, styleMask: .borderless, backing: .buffered, defer: false)

window.center()
let frame = window.frame
window.setFrameOrigin(CGPoint(x: frame.origin.x, y: 100))

How to remove duplicates based on property in array in Swift

Issue #441

Make object conform to Equatable and Hashable and use Set to eliminate duplications. Set loses order so we need to sort after uniquing

1
2
3
4
5
6
7
8
9
10
11
12
13
struct App: Equatable, Hashable {
static func == (lhs: App, rhs: App) -> Bool {
return lhs.name == rhs.name && lhs.bundleId == rhs.bundleId
}

func hash(into hasher: inout Hasher) {
hasher.combine(name)
hasher.combine(bundleId)
}
}

let uniqueApps = Array(Set(unsortedApps))
let apps = uniqueApps.sorted(by: { $0.name.lowercased() < $1.name.lowercased() })

How to log Error in Swift

Issue #439

Use localizedDescription

We need to provide NSLocalizedDescriptionKey inside user info dictionary, otherwise the outputt string may not be what we want.

NSError

https://developer.apple.com/documentation/foundation/nserror/1414418-localizeddescription

A string containing the localized description of the error.
The object in the user info dictionary for the key NSLocalizedDescriptionKey. If the user info dictionary doesn’t contain a value for NSLocalizedDescriptionKey, a default string is constructed from the domain and code.

1
2
3
4
5
6
7
let error = NSError(domain: "com.onmyway133.MyApp", code: 2, userInfo: [
"status_code": 2,
"status_message": "not enough power"
])

error.localizedDescription
// "The operation couldn’t be completed. (com.onmyway133.MyApp error 2.)"

Error

https://developer.apple.com/documentation/swift/error/2292912-localizeddescription

Retrieve the localized description for this error.

1
2
3
4
5
6
7
enum AppError: Error {
case request
case invalid
}

AppError.request.localizedDescription
// "The operation couldn’t be completed. (MyApp.AppError error 0.)"

Use describing String

To have better control, we can have toString which prints closly to what we expect

1
2
3
4
5
6
7
8
9
10
11
extension Error {
func toString() -> String {
return String(describing: self)
}
}

AppError.request.toString()
// request

nsError.toString()
// Error Domain=com.onmyway133.MyApp Code=2 "(null)" UserInfo={status_message=not enough power, status_code=SwiftGRPC.StatusCode.powerRequired}

How to handle NSTextField change in macOS

Issue #438

Storyboard

In Storyboard, NSTextField has an Action option that specify whether Send onSend on Enter only` should be the default behaviour.

textfield

Code

In code, NSTextFieldDelegate notifies whenever text field value changes, and target action notifies when Enter key is pressed

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

class ViewController: NSViewController, NSTextFieldDelegate {

@IBOutlet weak var textField: NSTextField!

override func viewDidLoad() {
super.viewDidLoad()

textField.delegate = self
}

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

Use EasyClosure

If we use EasyClosure then this is easy

1
2
3
4
5
6
7
8
9
let textField: NSTextField = 

textField.on.action { string in
print("User has pressed enter \(string)"
}

textField.on.change { string in
print("Text field value has changed")
}

Updated at 2020-11-27 07:39:43

How to add section header to NSCollectionView in macOS

Issue #437

Normal

Use Omnia for itemId extension

HeaderCell.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
final class HeaderCell: NSView, NSCollectionViewSectionHeaderView {
let label: NSTextField = withObject(NSTextField(labelWithString: "")) {
$0.textColor = R.color.header
$0.font = R.font.header
$0.alignment = .left
$0.lineBreakMode = .byTruncatingTail
}

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

addSubviews([label])

activate(
label.anchor.centerY,
label.anchor.left.constant(8)
)
}

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

ViewController.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
collectionView.register(
HeaderCell.self,
forSupplementaryViewOfKind: NSCollectionView.elementKindSectionHeader,
withIdentifier: HeaderCell.itemId
)

func collectionView(_ collectionView: NSCollectionView, viewForSupplementaryElementOfKind kind: NSCollectionView.SupplementaryElementKind, at indexPath: IndexPath) -> NSView {

if kind == NSCollectionView.elementKindSectionHeader {
let view = collectionView.makeSupplementaryView(
ofKind: kind,
withIdentifier: HeaderCell.itemId,
for: indexPath
) as! HeaderCell

let menu = app.menus[indexPath.section]
view.label.stringValue = menu.name

return view
} else {
return NSView()
}
}

In generic subclass

If use CollectionViewHandler from Omnia, then need to add @objc due to a bug in Swift compiler for subclassing generic class

1
@objc (collectionView:viewForSupplementaryElementOfKind:atIndexPath:)

MyHandler.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
import AppKit
import Omnia

class MyHandler: CollectionViewHandler<App.Key, KeyCell> {
override init() {
super.init()

layout.headerReferenceSize = NSSize(width: 300, height: 30)

collectionView.register(
HeaderCell.self,
forSupplementaryViewOfKind: NSCollectionView.elementKindSectionHeader,
withIdentifier: HeaderCell.itemId
)
}

@objc (collectionView:viewForSupplementaryElementOfKind:atIndexPath:)
func collectionView(_ collectionView: NSCollectionView, viewForSupplementaryElementOfKind kind: NSCollectionView.SupplementaryElementKind, at indexPath: IndexPath) -> NSView {

if kind == NSCollectionView.elementKindSectionHeader {
let view = collectionView.makeSupplementaryView(
ofKind: kind,
withIdentifier: HeaderCell.itemId,
for: indexPath
) as! HeaderCell

let menu = app.menus[indexPath.section]
view.label.stringValue = menu.name

return view
} else {
return NSView()
}
}
}

Use Omnia

Use CollectionViewSectionHandler from Omnia

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class SectionHandler: CollectionViewSectionHandler<App.Key, KeyCell, HeaderCell> {

}

sectionHandler.configureHeader = { section, view in
view.label.stringValue = section.name
}

sectionHandler.configure = { item, cell in
cell.shortcutLabel.stringValue = item.shortcut
cell.nameLabel.stringValue = item.name
}

sectionHandler.itemSize = { [weak self] in
guard let self = self else {
return .zero
}

let width = self.sectionHandler.collectionView.frame.size.width
- self.sectionHandler.layout.sectionInset.left
- self.sectionHandler.layout.sectionInset.right

return CGSize(width: width, height: 18)
}

Read more

How to show log in Apple Script

Issue #436

Open Script Editor, use log command and look for 4 tabs in bottom panel Result, Messages, Events and Replies

1
log "hello world"

How to show context menu from NSButton in macOS

Issue #435

Use NSMenu and popUp

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
func showQuitMenu() {
let menu = NSMenu()
let aboutItem = NSMenuItem(
title: "About",
action: #selector(onAboutTouched(_:)),
keyEquivalent: ""
)

let quitItem = NSMenuItem(
title: "Quit Hacker Pad",
action: #selector(onQuitTouched(_:)),
keyEquivalent: ""
)

aboutItem.target = self
quitItem.target = self

menu.addItem(aboutItem)
menu.addItem(quitItem)

menu.popUp(
positioning: aboutItem,
at: bottomView.quitButton.frame.origin,
in: bottomView
)
}

Use Omnia

1
2
3
4
5
6
7
8
9
10
let menuHandler = MenuHandler()
menuHandler.add(title: "About", action: {
NSWorkspace.shared.open(URL(string: "https://onmyway133.github.io/")!)
})

menuHandler.add(title: "Quit Hacker Pad", action: {
NSApp.terminate(nil)
})

menuHandler.show(from: self.bottomView.gearButton, in: self.bottomView)

How to handle UICollectionView reloadData with selected index path

Issue #434

When calling collectionView.reloadData(), selected indexpath stays the same, but be aware that order of data may have changed

1
2
3
4
5
6
7
8
let selectedData = ...
let indexPathForSelectedData = ...

collectionView.scrollToItem(
at: indexPathForSelectedData,
at: .centeredHorizontally,
animated: false
)

How to make checked NSButton in AppKit

Issue #433

  • Use Omnia for convenient style and isOn property
1
2
3
let checkButton = NSButton(checkboxWithTitle: "", target: nil, action: nil)
checkButton.stylePlain(title: "Autosave", color: R.color.text, font: R.font.text)
checkButton.isOn = true

How to save files in sandboxed macOS app

Issue #432

Read Container Directories and File System Access

When you adopt App Sandbox, your application has access to the following locations:

The app container directory. Upon first launch, the operating system creates a special directory for use by your app—and only by your app—called a container. Each user on a system gets an individual container for your app, within their home directory; your app has unfettered read/write access to the container for the user who ran it.

Use EasyStash

1
2
3
var options = Options()
options.searchPathDirectory = .documentDirectory
storage = try! Storage(options: options)

How to add AdMob to Android app

Issue #431

Use AdMob with Firebase

build.gradle

1
2
3
4
5
6
7
8
9
10
buildscript {
repositories {
google()
jcenter()

}
dependencies {
classpath 'com.google.gms:google-services:4.3.2'
}
}

app/build.gradle

1
2
3
4
5
6
7
8
9
10
11
12
13
class Version {
class Firebase {
static def analytics = "17.2.0"
static def ads = "18.2.0"
}
}

dependencies {
implementation "com.google.firebase:firebase-analytics:$Version.Firebase.analytics"
implementation "com.google.firebase:firebase-ads:$Version.Firebase.ads"
}

apply plugin: 'com.google.gms.google-services'

Manifest.xml

1
2
3
4
5
6
7
8
<manifest>
<application>
<!-- Sample AdMob App ID: ca-app-pub-3940256099942544~3347511713 -->
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="[ADMOB_APP_ID]"/>
</application>
</manifest>

MyApplication.kt

1
2
3
4
5
6
7
class MyApplication: Application() {
override fun onCreate() {
super.onCreate()

MobileAds.initialize(this)
}
}

AdView

fragment.xml

1
2
3
4
5
6
7
8
9
10
<com.google.android.gms.ads.AdView
xmlns:ads="http://schemas.android.com/apk/res-auto"
android:id="@+id/adView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
ads:adSize="BANNER"
ads:adUnitId="ca-app-pub-123456/123456"
ads:layout_constraintBottom_toBottomOf="parent"
ads:layout_constraintLeft_toLeftOf="parent"
ads:layout_constraintRight_toRightOf="parent"/>

Fragment.kt

1
2
3
4
5
import com.google.android.gms.ads.AdRequest
import com.google.android.gms.ads.AdView

val request = AdRequest.Builder().build()
adView.loadAd(request)

Troubleshooting

app/build.gradle

1
2
3
dependencies {
implementation 'com.google.android.gms:play-services-ads:18.2.0'
}

Cannot fit requested classes in a single dex file

app/build.gradle

1
2
3
4
5
6
7
8
9
10

android {
defaultConfig {
multiDexEnabled true
}
}

dependencies {
implementation 'com.android.support:multidex:1.0.3'
}

Read more

How to notarize electron app

Issue #430

Use electron builder

1
npm install electron-builder@latest --save-dev

package.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
37
{
"name": "icon_generator",
"version": "1.3.0",
"description": "A macOS app to generate app icons",
"main": "babel/main.js",
"repository": "https://github.com/onmyway133/IconGenerator",
"author": "Khoa Pham",
"license": "MIT",
"scripts": {
"start": "npm run babel && electron .",
"babel": "babel ./src --out-dir ./babel --copy-files",
"dist": "npm run babel && electron-builder"
},
"build": {
"appId": "com.onmyway133.IconGenerator",
"buildVersion": "20",
"productName": "Icon Generator",
"icon": "./Icon/Icon.icns",
"mac": {
"category": "public.app-category.productivity",
"hardenedRuntime": true,
"gatekeeperAssess": false,
"entitlements": "./entitlements.plist",
"entitlementsInherit": "./entitlements.plist"
},
"win": {
"target": "msi"
},
"linux": {
"target": [
"AppImage",
"deb"
]
},
"afterSign": "./afterSignHook.js"
}
}

Declare entitlements

entitlements.plist

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
</dict>
</plist>

Use electron-notarize

afterSignHook.js

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
const fs = require('fs');
const path = require('path');
var electron_notarize = require('electron-notarize');

module.exports = async function (params) {
// Only notarize the app on Mac OS only.
if (process.platform !== 'darwin') {
return;
}
console.log('afterSign hook triggered', params);

// Same appId in electron-builder.
let appId = 'com.onmyway133.IconGenerator'

let appPath = path.join(params.appOutDir, `${params.packager.appInfo.productFilename}.app`);
if (!fs.existsSync(appPath)) {
throw new Error(`Cannot find application at: ${appPath}`);
}

console.log(`Notarizing ${appId} found at ${appPath}`);

try {
await electron_notarize.notarize({
appBundleId: appId,
appPath: appPath,
appleId: process.env.appleId,
appleIdPassword: process.env.appleIdPassword,
});
} catch (error) {
console.error(error);
}

console.log(`Done notarizing ${appId}`);
};

Run

Generate password for Apple Id because of 2FA

1
2
3
export appleId=onmyway133@gmail.com
export appleIdPassword=1234-abcd-efgh-7890
npm run dist

Check

1
spctl --assess --verbose Icon\ Generator.app

Troubleshooting

babel

  • Since electron-builder create dist folder for distribution, for example dist/mac/Icon Generator, I’ve renamed babel generated code to babel directory

babel 6 regeneratorRuntime is not defined

It is because of afterSignHook. Ignore in .babelrc not work

1
2
3
4
5
6
7
8
9
{
"plugins": [
"transform-react-jsx-source"
],
"presets": ["env", "react"],
"ignore": [
"afterSignHook.js"
]
}

Should use babel 7 with babel.config.js

1
2
npm install --save @babel/runtime 
npm install --save-dev @babel/plugin-transform-runtime

Use electron-forge

https://httptoolkit.tech/blog/notarizing-electron-apps-with-electron-forge/

Read more

How to use marked in WKWebView in AppKit

Issue #429

Use https://github.com/markedjs/marked

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>Marked in the browser</title>
</head>
<body>
<div id="content"></div>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script>
document.getElementById('content').innerHTML =
marked(`# Marked in the browser\n\nRendered by **marked**.`);
</script>
</body>
</html>

Should use back tick instead of ' to deal with multiline string, and escape back tick

1
2
3
4
let markdown = markdown.replacingOccurrences(of: "`", with: "\\`")
marked(`\(markdown)`);

webView.loadHTMLString(string, baseURL: nil)

For WKWebView to load even local content, need to enable Outgoing connections, and maybe Incoming connections in Sandbox

How to enable NSMenuItem in AppKit

Issue #428

Need to set target

1
2
3
4
5
6
7
let item = NSMenuItem(
title: title,
action: #selector(onMenuItemClicked(_:)),
keyEquivalent: ""
)

item.target = self

Sometimes, need to check autoenablesItems

Indicates whether the menu automatically enables and disables its menu items.

This property contains a Boolean value, indicating whether the menu automatically enables and disables its menu items. If set to true, menu items of the menu are automatically enabled and disabled according to rules computed by the NSMenuValidation informal protocol. By default, NSMenu objects autoenable their menu items.

How to use generic NSCollectionView in macOS

Issue #427

See CollectionViewHandler

Use ClickedCollectionView to detect clicked index for context menu.
Embed NSCollectionView inside NSScrollView to enable scrolling

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

public class CollectionViewHandler<Item: Equatable, Cell: NSCollectionViewItem>
: NSObject, NSCollectionViewDataSource, NSCollectionViewDelegateFlowLayout {

public let layout = NSCollectionViewFlowLayout()
public let scrollView = NSScrollView()
public let collectionView = ClickedCollectionView()

public var items = [Item]()
public var itemSize: () -> CGSize = { .zero }
public var configure: (Item, Cell) -> Void = { _, _ in }

override init() {
super.init()

layout.minimumLineSpacing = 4
layout.sectionInset = NSEdgeInsets(top: 4, left: 4, bottom: 4, right: 4)

collectionView.dataSource = self
collectionView.delegate = self
collectionView.collectionViewLayout = layout
collectionView.allowsMultipleSelection = false
collectionView.backgroundColors = [.clear]
collectionView.isSelectable = true

collectionView.register(Cell.self, forItemWithIdentifier: Cell.itemId)

scrollView.documentView = collectionView
}

// MARK: - Items

public func add(item: Item) {
items.insert(item, at: 0)
let indexPath = IndexPath(item: 0, section: 0)
collectionView.animator().insertItems(at: Set(arrayLiteral: indexPath))
}

public func remove(item: Item) {
guard let index = items.firstIndex(where: { $0 == item }) else {
return
}

remove(index: index)
}

public func remove(index: Int) {
items.remove(at: index)
let indexPath = IndexPath(item: index, section: 0)
collectionView.animator().deleteItems(at: Set(arrayLiteral: indexPath))
}

// MARK: - NSCollectionViewDataSource

public func numberOfSections(in collectionView: NSCollectionView) -> Int {
return 1
}

public func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int {
return items.count
}

public func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {

let cell = collectionView.makeItem(withIdentifier: Cell.itemId, for: indexPath) as! Cell
let item = items[indexPath.item]
configure(item, cell)
return cell
}

// MARK: - NSCollectionViewDelegateFlowLayout

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

return itemSize()
}
}

How to use throttle and debounce in RxSwift

Issue #426

throttle

https://rxmarbles.com/#throttle

throttle

Returns an Observable that emits the first and the latest item emitted by the source Observable during sequential time windows of a specified duration.
This operator makes sure that no two elements are emitted in less then dueTime.

1
.throttle(.milliseconds(500), scheduler: MainScheduler.instance)

In a time window, only the first item gets emitted.

💡 In other words, in a time window, take first and discard following.

For example, when failure, we show error message but don’t want to show error messages consecutively. We can use throttle to discard consecutive errors.

1
2
3
viewModel.fetchBooksFail
.observeOn(MainScheduler.instance)
.throttle(.seconds(2), scheduler: MainScheduler.instance)

debounce

https://rxmarbles.com/#debounce

debounce

Ignores elements from an observable sequence which are followed by another element within a specified relative time duration, using the specified scheduler to run throttling timers.

1
.debounce(.milliseconds(500), scheduler: MainScheduler.instance)

If an element is about to get emitted, wait for a time window to see if there are other elements emitted. If yes, start the waiting again.

💡 In other words, in a time window, wait and take last

For example, when receiving data, we may need to wait for the final data if there are many data events emitted consecutively

1
2
3
viewModel.fetchBooks
.filter({ !$0.isEmpty })
.debounce(.milliseconds(500), scheduler: MainScheduler.instance)

How to use Firebase ID token

Issue #424

One confusing point here that people often do not realize is that even though the custom token itself expires after one hour, a modern client SDK authenticated with that custom token will stay authenticated beyond that hour! What happens under the hood is that the custom token is sent to the Firebase Auth service in exchange for an ID token and refresh token pair which are used to keep the client SDK authenticated

As with custom tokens, ID tokens are short-lived JWTs, lasting for just one hour. In order to allow end users to stay logged in for more than one hour, the modern SDKs transparently refresh a user’s ID token on your behalf using a refresh token

If your app includes a custom backend server, ID tokens can and should be used to communicate securely with it. Instead of sending requests with a user’s raw uid which can be easily spoofed by a malicious client, send the user’s ID token which can be verified via a Firebase Admin SDK

When a user or device successfully signs in, Firebase creates a corresponding ID token that uniquely identifies them and grants them access to several resources, such as Firebase Realtime Database and Cloud Storage. You can re-use that ID token to identify the user or device on your custom backend server. To retrieve the ID token from the client, make sure the user is signed in and then get the ID token from the signed-in user: