How to use Firebase in macOS

Issue #501

  • Use Catalyst
  • Add to CocoaPods
1
2
3
4
5
6
7
8
9
platform :ios, '13.0'

target 'MyApp' do
use_frameworks!

pod 'FirebaseCore'
pod 'Firebase/Firestore'

end

Troubleshooting

Select a team for gRPC-C++-gRPCCertificates-Cpp

Screenshot 2019-11-12 at 14 53 03

FIRAnalyticsConnector: building for Mac Catalyst, but linking in object file built for iOS Simulator

https://stackoverflow.com/questions/57666155/firanalyticsconnector-building-for-mac-catalyst-but-linking-in-object-file-bui

The problem was related to the difference between Firebase/Core and FirebaseCore. The first is a subspec of the Firebase pod that depends on FirebaseAnalytics. The second is only the FirebaseCore pod. Only the latter should be used for macOS.

How to use Firebase RemoteConfig

Issue #493

Declare in Podfile

1
2
pod 'Firebase/Core'
pod 'Firebase/RemoteConfig'

Use RemoteConfigHandler to encapsulate logic. We introduce Key with CaseIterable and defaultValue of type NSNumber to manage default values.

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
import Firebase
import FirebaseRemoteConfig

final class RemoteConfigHandler {
let remoteConfig: RemoteConfig

enum Key: String, CaseIterable {
case interval = "fetch_interval"

var defaultValue: NSNumber {
switch self {
case .periodicGetSalons: return NSNumber(value: 300)
}
}
}

init() {
self.remoteConfig = RemoteConfig.remoteConfig()

let settings = RemoteConfigSettings()
settings.minimumFetchInterval = 0
self.remoteConfig.configSettings = settings

let map = Key.allCases.reduce([String: NSObject](), { map, key in
var map = map
map[key.rawValue] = key.defaultValue
return map
})

self.remoteConfig.setDefaults(map)
}

func update() {
self.remoteConfig.fetchAndActivate(completionHandler: { status, error in
print(status, error as Any)
})
}

var fetchInterval: TimeInterval {
return getValue(key: .interval, transform: { $0.doubleValue })
}

private func getValue<T>(key: Key, transform: (NSNumber) -> T) -> T {
let number = remoteConfig.configValue(forKey: key.rawValue).numberValue ?? key.defaultValue
return transform(number)
}
}

How to show image picker in SwiftUI

Issue #485

The easiest way to show image picker in iOS is to use UIImagePickerController, and we can bridge that to SwiftUI via UIViewControllerRepresentable

First attempt, use Environment

We conform to UIViewControllerRepresentable and make a Coordinator, which is the recommended way to manage the bridging with UIViewController.

There’s some built in environment property we can use, one of those is presentationMode where we can call dismiss to dismiss the modal.

My first attempt looks like below

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

public struct ImagePicker: UIViewControllerRepresentable {
@Environment(\.presentationMode) private var presentationMode
@Binding var image: UIImage?

public func makeCoordinator() -> ImagePicker.Coordinator {
return ImagePicker.Coordinator(
presentationMode: presentationMode,
image: $image
)
}

public func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let controller = UIImagePickerController()
controller.delegate = context.coordinator
return controller
}

public func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
// No op
}
}

public extension ImagePicker {
class Coordinator: NSObject, UINavigationControllerDelegate {
@Binding var presentationMode: PresentationMode
@Binding var image: UIImage?

public init(presentationMode: Binding<PresentationMode>, image: Binding<UIImage?>) {
self._presentationMode = presentationMode
self._image = image
}
}
}

extension ImagePicker.Coordinator: UIImagePickerControllerDelegate {
public func imagePickerController(
_ picker: UIImagePickerController,
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
self.image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage
presentationMode.dismiss()
}

public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
presentationMode.dismiss()
}
}

Signatures

We need to be aware of the types of these property wrappers

Where we declare environment, presentationMode is of type Binding<PresentationMode>

1
@Environment(\.presentationMode) private var presentationMode

Given a Binding declaration, for example @Binding var image: UIImage?, image is of type UIImage? but $image is Binding<UIImage?>

1
2
3
4
5
6
public func makeCoordinator() -> ImagePicker.Coordinator {
return ImagePicker.Coordinator(
image: $image,
isPresented: $isPresented
)
}

When we want to assign to variables in init, we use _image to use mutable Binding<UIImage?> because self.$image gives us immutable Binding<UIImage?>

1
2
3
4
5
6
7
8
9
class Coordinator: NSObject, UINavigationControllerDelegate {
@Binding var presentationMode: PresentationMode
@Binding var image: UIImage?

public init(presentationMode: Binding<PresentationMode>, image: Binding<UIImage?>) {
self._presentationMode = presentationMode
self._image = image
}
}

How to use

To show modal, we use sheet and use a state @State var showImagePicker: Bool = false to control its presentation

1
2
3
4
5
6
7
8
Button(action: {
self.showImagePicker.toggle()
}, label: {
Text("Choose image")
})
.sheet(isPresented: $showImagePicker, content: {
ImagePicker(image: self.$image)
})

Environment outside body

If we run the above code, it will crash because of we access environment value presentationMode in makeCoordinator and this is outside body

Fatal error: Reading Environment<Binding> outside View.body

1
2
3
4
5
6
public func makeCoordinator() -> ImagePicker.Coordinator {
return ImagePicker.Coordinator(
presentationMode: presentationMode,
image: $image
)
}

Second attempt, pass closure

So instead of passing environment presentationMode, we can pass closure, just like in React where we pass functions to child component.

So ImagePicker can just accept a closure called onDone, and the component that uses it can do the dismissal.

1
2
3
4
5
6
7
8
9
10
Button(action: {
self.showImagePicker.toggle()
}, label: {
Text("Choose image")
})
.sheet(isPresented: $showImagePicker, content: {
ImagePicker(image: self.$image, onDone: {
self.presentationMode.wrappedValue.dismiss()
})
})

Unfortunately, although the onDone gets called, the modal is not dismissed.

Use Binding instead of Environment

Maybe there are betters way, but we can use Binding to replace usage of Environment.

We can do that by accepting Binding and change the isPresented state

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

public struct ImagePicker: UIViewControllerRepresentable {
@Binding var image: UIImage?
@Binding var isPresented: Bool

public func makeCoordinator() -> ImagePicker.Coordinator {
return ImagePicker.Coordinator(
image: $image,
isPresented: $isPresented
)
}

public func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let controller = UIImagePickerController()
controller.delegate = context.coordinator
return controller
}

public func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
// No op
}
}

public extension ImagePicker {
class Coordinator: NSObject, UINavigationControllerDelegate {
@Binding var isPresented: Bool
@Binding var image: UIImage?

public init(image: Binding<UIImage?>, isPresented: Binding<Bool>) {
self._image = image
self._isPresented = isPresented
}
}
}

extension ImagePicker.Coordinator: UIImagePickerControllerDelegate {
public func imagePickerController(
_ picker: UIImagePickerController,
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
self.image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage
isPresented = false
}

public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
isPresented = false
}
}

How to use it

1
2
3
4
5
6
7
8
Button(action: {
self.showImagePicker.toggle()
}, label: {
Text("Choose image")
})
.sheet(isPresented: $showImagePicker, content: {
ImagePicker(image: self.$image, isPresented: self.$showImagePicker)
})

Pass ImagePicker to Coordinator

So that we can call parent.presentationMode.wrappedValue.dismiss()

How to use array of strings in ForEach in SwiftUI

Issue #483

Every item in list must be uniquely identifiable

1
2
3
4
5
6
7
8
9
10
11
12
List {
ForEach(books, id: \.bookId) { book in
NavigationLink(destination:
BookView(book: book)
.navigationBarTitle(book.name)
) {
VStack {
Text(book.name)
}
}
}
}

In case of primitive, we can just provide id to conform to Identifiable

1
2
3
4
5
extension String: Identifiable {
public var id: String {
return self
}
}

How to make ISO 8601 date in Swift

Issue #479

From ISO8601 spec, the problems are the representation and time zone

1
2
3
4
5
6
7
ISO 8601 = year-month-day time timezone
For date and time, there are basic (YYYYMMDD, hhmmss, ...) and extended format (YYYY-MM-DD, hh:mm:ss, ...)
Time zone can be Zulu, offset or GMT
Separator for date and time can be space, or T
There are week format for date, but it is rarely used
Timezone can be a lot of spaces after
Second is optional

Here are some valid strings

1
2
3
4
5
2016-04-08T10:25:30Z
2016-04-08 11:25:30+0100
2016-04-08 202530GMT+1000
20160408 08:25:30-02:00
2016-04-08 11:25:30 +0100

Solutions

Use NSDateFormatter and normalize the date string.

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
public var stringToDateFormatter: DateFormatter = {
let formatter = Foundation.DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyyMMdd HHmmssZ"

return formatter
}()

public func date(string: String) -> Date? {
var basicString = string

if let regex = try? NSRegularExpression(pattern: "[0-9]{4}-[0-9]{2}-[0-9]{2}", options: []),
let result = regex.firstMatch(in: string, options: .anchored, range: NSMakeRange(0, string.characters.count)) {
basicString = (basicString as NSString).replacingOccurrences(of: "-", with: "", options: [], range: result.range)
}

basicString = basicString
.replacingOccurrences(of: ":", with: "")
.replacingOccurrences(of: "GMT", with: "")
.replacingOccurrences(of: "T", with: " ")
.replacingOccurrences(of: ",", with: ".")

return stringToDateFormatter.date(from: basicString)
?? stringToDateMillisecondsFormatter.date(from: basicString)
}

So here is the format that I’m using in my ISO8601

1
2
3
let formatter = NSDateFormatter()
formatter.locale = NSLocale(localeIdentifier: "en_US_POSIX")
formatter.dateFormat = "yyyyMMdd HHmmssZ"

About the Z identifier Date Field Symbol Table

Z: The ISO8601 basic format with hours, minutes and optional seconds fields. The format is equivalent to RFC 822 zone format (when optional seconds field is absent)

About locale Formatting Data Using the Locale Settings

Locales represent the formatting choices for a particular user, not the user’s preferred language. These are often the same but can be different. For example, a native English speaker who lives in Germany might select English as the language and Germany as the region

About en_US_POSIX Technical Q&A QA1480 NSDateFormatter and Internet Dates

On the other hand, if you’re working with fixed-format dates, you should first set the locale of the date formatter to something appropriate for your fixed format. In most cases the best locale to choose is “en_US_POSIX”, a locale that’s specifically designed to yield US English results regardless of both user and system preferences.

“en_US_POSIX” is also invariant in time (if the US, at some point in the future, changes the way it formats dates, “en_US” will change to reflect the new behaviour, but “en_US_POSIX” will not), and between machines (“en_US_POSIX” works the same on iOS as it does on OS X, and as it it does on other platforms).

NSISO8601DateFormatter from iOS 10

From iOS 10, we can use NSISO8601DateFormatter

The NSISO8601DateFormatter class generates and parses string representations of dates following the ISO 8601 standard. Use this class to create ISO 8601 representations of dates and create dates from text strings in ISO 8601 format.

Code

How to check platform versions in Swift

Issue #477

Mark APIs availability

1
2
3
4
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public extension View {

}

Check platform

1
2
3
4
5
#if canImport(UIKit)
import UIKit
#elseif canImport(OSX)
import AppKit
#endif

In watchOS app, it still can import UIKit, so for only iOS usage, we need to use os check

1
#if canImport(UIKit) && os(iOS)

Check environment

1
2
3
4
5
#if targetEnvironment(macCatalyst)
print("UIKit running on macOS")
#else
print("Your regular code")
#endif
1
2
3
4
5
#if targetEnvironment(simulator)
// your simulator code
#else
// your real device code
#endif

How to flick using UIKit Dynamic in iOS

Issue #475

For a snack bar or image viewing, it’s handy to be able to just flick or toss to dismiss

We can use UIKit Dynamic, which was introduced in iOS 7, to make this happen.

Use UIPanGestureRecognizer to drag view around, UISnapBehavior to make view snap back to center if velocity is low, and UIPushBehavior to throw view in the direction of the gesture.

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

final class FlickHandler {
private let viewToMove: UIView
private let referenceView: UIView
private var panGR: UIPanGestureRecognizer!
private let animator: UIDynamicAnimator

private var snapBehavior: UISnapBehavior?
private var pushBehavior: UIPushBehavior?
private let debouncer = Debouncer(delay: 0.5)

var onFlick: () -> Void = {}

init(viewToMove: UIView, referenceView: UIView) {
self.viewToMove = viewToMove
self.referenceView = referenceView
self.animator = UIDynamicAnimator(referenceView: referenceView)
self.panGR = UIPanGestureRecognizer(target: self, action: #selector(handleGesture(_:)))
viewToMove.addGestureRecognizer(panGR)
}

@objc private func handleGesture(_ gr: UIPanGestureRecognizer) {
switch gr.state {
case .began:
handleBegin()
case .changed:
handleChange(gr)
default:
handleEnd(gr)
}
}

private func handleBegin() {
animator.removeAllBehaviors()
}

private func handleChange(_ gr: UIPanGestureRecognizer) {
let translation = panGR.translation(in: referenceView)
viewToMove.transform = CGAffineTransform(
translationX: translation.x,
y: translation.y
)
}

private func handleEnd(_ gr: UIPanGestureRecognizer) {
let velocity = gr.velocity(in: gr.view)
let magnitude = sqrt((velocity.x * velocity.x) + (velocity.y * velocity.y))
if magnitude > 1000 {
animator.removeAllBehaviors()

let pushBehavior = UIPushBehavior(items: [viewToMove], mode: .instantaneous)
pushBehavior.pushDirection = CGVector(dx: velocity.x, dy: velocity.y)
pushBehavior.magnitude = magnitude / 35

self.pushBehavior = pushBehavior
animator.addBehavior(pushBehavior)

onFlick()
debouncer.run { [weak self] in
self?.animator.removeAllBehaviors()
}
} else {
let snapBehavior = UISnapBehavior(
item: viewToMove,
snapTo: viewToMove.center
)

self.snapBehavior = snapBehavior
animator.addBehavior(snapBehavior)
}
}
}

How to use Swift package manager in watchOS

Issue #474

SPM

Go to Project -> Swift Packages, add package. For example https://github.com/onmyway133/EasyStash

Select your WatchKit Extension target, under Frameworks, Libraries and Embedded Content add the library

CocoaPods

If we use CocoaPods, then it needs to be in WatchKit Extension

1
2
3
4
target 'MyApp WatchKit Extension' do
use_frameworks!
pod 'EasyStash', :git => 'https://github.com/onmyway133/EasyStash'
end

How to use external display in iOS

Issue #473

Before iOS 13

Use UIScreen.didConnectNotification

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
NotificationCenter.default.addObserver(forName: UIScreen.didConnectNotification,
object: nil, queue: nil) { (notification) in
// Get the new screen information.
let newScreen = notification.object as! UIScreen
let screenDimensions = newScreen.bounds

// Configure a window for the screen.
let newWindow = UIWindow(frame: screenDimensions)
newWindow.screen = newScreen
// Install a custom root view controller in the window.
self.configureAuxilliaryInterface(with: newWindow)

// You must show the window explicitly.
newWindow.isHidden = false
// Save a reference to the window in a local array.
self.additionalWindows.append(newWindow)}

From iOS 13

Handle configurationForConnecting

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
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

var newWindow: UIWindow!

// MARK: UISceneSession Lifecycle

func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.


print(UIApplication.shared.connectedScenes)
let scene = UIWindowScene(session: connectingSceneSession, connectionOptions: options)

// Configure a window for the screen.
self.newWindow = UIWindow(frame: CGRect(x: 0, y: 0, width: 1000, height: 500))
// self.newWindow.backgroundColor = UIColor.yellow
// Install a custom root view controller in the window.

let viewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(identifier: "other") as! OtherViewController
self.newWindow.rootViewController = viewController
self.newWindow.windowScene = scene

// You must show the window explicitly.
self.newWindow.isHidden = false

return UISceneConfiguration(name: "External configuration", sessionRole: connectingSceneSession.role)
}
}

Or we can declare in plist

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
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
<key>UIWindowSceneSessionRoleExternalDisplay</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>External configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).OtherSceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Other</string>
</dict>
</array>
</dict>
</dict>

Read more

How to show error message like Snack Bar in iOS

Issue #472

Build error view

Use convenient code from Omnia

To make view height dynamic, pin UILabel to edges and center

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import UIKit

final class ErrorMessageView: UIView {
let box: UIView = {
let view = UIView()
view.backgroundColor = R.color.primary
view.layer.cornerRadius = 6
return view
}()

let label: UILabel = {
let label = UILabel()
label.styleAsText()
label.textColor = R.color.darkText
label.numberOfLines = 0
return label
}()

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

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

private func setup() {
addSubviews([box, label])
NSLayoutConstraint.on([
box.pinEdges(view: self, inset: UIEdgeInsets.all(16)),
label.pinEdges(view: box, inset: UIEdgeInsets.all(8))
])

NSLayoutConstraint.on([
box.heightAnchor.constraint(greaterThanOrEqualToConstant: 48)
])

NSLayoutConstraint.on([
label.centerYAnchor.constraint(equalTo: centerYAnchor)
])
}
}

Show and hide

Use Auto Layout and basic UIView animation. Use debouncer to avoid hide gets called for the new show. Use debouncer instead of DispatchQueue.main.asyncAfter because it can cancel the previous DispatchWorkItem

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

final class ErrorMessageHandler {
let view: UIView
let errorMessageView = ErrorMessageView()
let debouncer = Debouncer(delay: 0.5)

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

func show(text: String) {
self.errorMessageView.label.text = text
view.addSubview(errorMessageView)
NSLayoutConstraint.on([
errorMessageView.leftAnchor.constraint(equalTo: view.leftAnchor),
errorMessageView.rightAnchor.constraint(equalTo: view.rightAnchor),
errorMessageView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
])

toggle(shows: true)
debouncer.run {
self.hide()
}
}

func hide() {
toggle(shows: false)
}

private func toggle(shows: Bool) {
self.errorMessageView.alpha = shows ? 0 : 1.0
UIView.animate(withDuration: 0.25, animations: {
self.errorMessageView.alpha = shows ? 1.0 : 0
}, completion: { _ in
if shows {
self.view.bringSubviewToFront(self.errorMessageView)
} else {
self.errorMessageView.removeFromSuperview()
}
})
}
}

Handle keyboard

If we add this error message on UIView in ViewController and we use KeyboardHandler to scroll the entire view, then this snack bar will move up as well

1
2
3
4
5
6
7
8
9
10
final class ErrorMessageHandler {
private let errorMessageView = ErrorMessageView()
private var view = UIView()
private var bottomOffset: CGFloat = 0

func on(view: UIView, bottomOffset: CGFloat) {
self.view = view
self.bottomOffset = bottomOffset
}
}

UIView animation completion

One tricky thing is that if we call hide and then show immediately, the completion of hide will be called after and then remove the view.

When we start animation again, the previous animation is not finished, so we need to check

Read UIView.animate

completion
A block object to be executed when the animation sequence ends. This block has no return value and takes a single Boolean argument that indicates whether or not the animations actually finished before the completion handler was called. If the duration of the animation is 0, this block is performed at the beginning of the next run loop cycle. This parameter may be NULL.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private func toggle(shows: Bool) {
self.errorMessageView.alpha = shows ? 0 : 1.0
UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseInOut, animations: {
self.errorMessageView.alpha = shows ? 1.0 : 0
}, completion: { finished in
if shows {
self.view.bringSubviewToFront(self.errorMessageView)
} else if finished {
self.errorMessageView.removeFromSuperview()
} else {
// No op
}
})
}

How to hide tab bar when push in iOS

Issue #471

1
2
let navigationController = UINavigationController(rootViewController: viewControllerA)
navigationController.pushViewController(viewControllerB, animated: true)

In view controller B, need to set hidesBottomBarWhenPushed in init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
final class ViewControllerB: UIViewController {
let mainView = EditPaymentMethodView()
var scenario: PaymentMethodScenario!

init() {
super.init(nibName: nil, bundle: nil)

hidesBottomBarWhenPushed = true
}

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

How to add trailing image to UILabel in iOS

Issue #470

Use NSTextAttachment inside NSAttributedString

1
2
3
4
5
6
7
8
9
10
11
12
extension UILabel {
func addTrailing(image: UIImage) {
let attachment = NSTextAttachment()
attachment.image = image

let attachmentString = NSAttributedString(attachment: attachment)
let string = NSMutableAttributedString(string: self.text!, attributes: [:])

string.append(attachmentString)
self.attributedText = string
}
}

How to handle different states in a screen in iOS

Issue #469

If there are lots of logics and states inside a screen, it is best to introduce parent and child container, and switch child depends on state. Each child acts as a State handler.

In less logic case, we can introduce a Scenario class that holds the state. So the ViewController can be very slim. The thing with State is that all possible scenarios are clear and required to be handled

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
final class UserDetailScenario {
enum State {
case unknown
case getUser(Email)
case newUser(Email)
case existingUser(User)
case failure(Error)
}

var state: State = .unknown(nil) {
didSet {
self.reload()
}
}

private func reload() {
switch state {
case .unknown:
handleUnknown()
case .getUser(let email):
handleGetUser(email: email)
case .newUser(let email):
handleNewUser(email: email)
case .existingUser(let user):
handleExistingUser(user: user
case .failure(let error):
logError(error)
}
}

How to make generic store for Codable in Swift

Issue #465

Use EasyStash

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

final class Store<T: Codable & ItemProtocol>: Codable {
var items = [T]()

func bookmark(item: T) {
items.append(item)
}

func unbookmark(item: T) {
guard let index = items.firstIndex(where: { $0.itemId == item.itemId }) else {
return
}

items.remove(at: index)
}

func isBookmark(item: T) -> Bool {
return items.contains(where: { $0.itemId == item.itemId })
}
}
1
2
3
4
5
6
7
8
9
10
11
12
import EasyStash

final class StoreContainer {
var food: Store<Food>

static var shared = StoreContainer()
let storage = try! Storage(options: Options())

init() {
food = try? storage.load(forKey: "food", as: Store<Food>.self) ?? Store<Food>()
}
}

If Swift has problem compiling because of generic, use try catch to declare in multiple steps in stead of ??

1
2
3
4
5
6
7
init() {
do {
self.food = try storage.load(forKey: "food", as: Store<Food>.self)
} catch {
self.food = Store<Food>()
}
}

How to zoom to fit many coordinates in Google Maps in iOS

Issue #463

1
2
3
4
5
func zoom(location1: CLLocation, location2: CLLocation) {
let bounds = GMSCoordinateBounds(coordinate: location1.coordinate, coordinate: location2.coordinate)
let update = GMSCameraUpdate.fit(bounds, withPadding: 16)
mapView.animate(with: update)
}

How to use Codable to store preferences in Swift

Issue #462

Using object, we don’t need to care about nil vs false like in UserDefaults, our object is the source of truth

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class StoringHandler<T: Codable> {
private let key: Storage.Keys
private let storage = Deps.storage

init(key: Storage.Keys) {
self.key = key
load()
}

var object: T? {
didSet {
if let object = object {
try? storage.save(object: object, key: key)
} else {
try? storage.clear(key: key)
}
}
}

private func load() {
self.object = try? storage.load(key: key, as: T.self)
}
}

Then subclass StoringHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct OnboardInfo: Codable {
let hasOnboarded: Bool
}

final class OnboardingHandler: StoringHandler<OnboardInfo> {
let storage = Deps.storage

var hasOnboarded: Bool {
get {
return object?.hasOnboarded ?? false
}
set {
object = OnboardInfo(hasOnboarded: newValue)
}
}
}

How to custom UIAlertController in iOS

Issue #461

With UIAlertController we can add buttons and textfields, and just that

1
2
func addAction(UIAlertAction)
func addTextField(configurationHandler: ((UITextField) -> Void)?)

To add custom content to UIAlertController, there are some workarounds

Add content onto UITextField

Restyle UITextField and add custom content

Subclass UIAlertController and handle UI in viewWillAppear

By subclassing we can tweak the whole view hierarchy and listen to events like layout subviews, but this can be very fragile.

Make custom UIViewController that looks like UIAlertController

This is the correct way but takes too much time to imitate UIAlertController, and have to deal with UIVisualEffectView, resize view for different screen sizes and dark mode

Add view to UIAlertController view

We can detect UILabel to add our custom view below it, using How to find subview recursively in Swift

But a less work approach is to add it above buttons, this is easily achieved by offsetting bottom.

alert

The trick is here is to have line breaks in message to leave space for our custom view.

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
let controller = UIAlertController(
title: title,
message: message + String(repeating: "\n", count: 10),
preferredStyle: .alert
)

let continueAction = UIAlertAction(
title: R.string.localizable.continue(),
style: .default) { _ in
action()
}

let cancelAction = UIAlertAction(title: R.string.localizable.cancel(), style: .cancel)
controller.addAction(cancelAction)
controller.addAction(continueAction)

let customView = UIView()
customView.backgroundColor = UIColor.green

controller.view.addSubview(customView)
NSLayoutConstraint.on([
customView.leftAnchor.constraint(equalTo: controller.view.leftAnchor),
customView.rightAnchor.constraint(equalTo: controller.view.rightAnchor),
customView.bottomAnchor.constraint(equalTo: controller.view.bottomAnchor, constant: -50),
customView.heightAnchor.constraint(equalToConstant: 100)
])

How to find subview recursively in Swift

Issue #460

1
2
3
4
5
6
7
8
9
10
11
12
13
extension UIView {
func findRecursively<T: UIView>(type: T.Type, match: (T) -> Bool) -> T? {
for view in subviews {
if let subview = view as? T, match(subview) {
return subview
} else {
return view.findRecursively(type: type, match: match)
}
}

return nil
}
}

How to use provisional notification in iOS 12

Issue #456

From WWDC 2018 What’s New in User Notifications

Instead, the notifications from your app will automatically start getting delivered.

Notifications that are delivered with provisional authorization will have a prompt like this on the notification itself. And this will help the users decide after having received a few notifications whether they want to keep getting these notifications or whether they want to turn them off

It’s an automatic trial of the notifications from your app to help your users make a more informed decision about whether they want these notifications.

1
2
3
notificationCenter.requestAuthorization(  
options:[.badge, .sound, .alert, .provisional]) {
}

From http://info.localytics.com/blog/ios-12-brings-new-power-to-push-notifications

Provisional Authorization takes advantage of another new feature in iOS 12: the ability for messages to be “delivered quietly.” When a notification is delivered quietly, it can only be seen in the iOS Notification Center, which the user accesses by swiping down from the top of their phone. They don’t appear as banners or show up on the lock screen. As you might have guessed, quiet notifications also don’t make a sound.

If a user taps the “Keep” button, they can decide whether they want your app’s notifications to start getting delivered prominently (i.e. fully opt-in to push notifications) or continue to receive them quietly (i.e. pushes continue to get sent directly to the Notification Center).

The intent of Provisional Authorization is to give users a trial run with your app’s notifications. Apple created Provisional Authorization because it realized that it’s impossible for users to make an informed choice about whether or not they want to receive push notifications from an app until they’ve seen what kinds of messages the app is going to send them.


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

How to check if push notification is actually enabled in iOS

Issue #455

There are times we want to log if user can receive push notification. We may be tempted to merely use isRegisteredForRemoteNotifications but that is not enough. From a user ‘s point of view, they can either receive push notification or not. But behind the scene, many factors are in the game. It can be that user has disabled push notification in app settings or in iOS Settings. It can also be that user enables push notification but disables all sound or banner display mechanism.

isRegisteredForRemoteNotifications is that your app has connected to APNS and get device token, this can be for silent push notification
currentUserNotificationSettings is for user permissions, without this, there is no alert, banner or sound push notification delivered to the app
Here is the check

1
2
3
4
5
6
7
8
9
static var isPushNotificationEnabled: Bool {
guard let settings = UIApplication.shared.currentUserNotificationSettings
else {
return false
}

return UIApplication.shared.isRegisteredForRemoteNotifications
&& !settings.types.isEmpty
}

For iOS 10, with the introduction of UserNotifications framework, instead of checking for currentUserNotificationSettings, you should use UserNotifications framework

1
2
3
4
5
6
7
8
9
10
center.getNotificationSettings(completionHandler: { settings in
switch settings.authorizationStatus {
case .authorized, .provisional:
print("authorized")
case .denied:
print("denied")
case .notDetermined:
print("not determined, ask user for permission now")
}
})

Push notification can be delivered to our apps in many ways, and we can ask for that

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

User can go to Settings app and turn off any of those at any time, so it’s best to check for that in the settings object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
open class UNNotificationSettings : NSObject, NSCopying, NSSecureCoding {


open var authorizationStatus: UNAuthorizationStatus { get }


open var soundSetting: UNNotificationSetting { get }

open var badgeSetting: UNNotificationSetting { get }

open var alertSetting: UNNotificationSetting { get }


open var notificationCenterSetting: UNNotificationSetting { get }
}

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

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 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))