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 multiline Text in SwiftUI in watchOS

Issue #482

lineLimit does not seem to work, use fixedSize instead

Fixes this view at its ideal size.

A view that fixes this view at its ideal size in the dimensions given in fixedDimensions.

1
2
3
4
5
6
7
8
9
extension Text {
func styleText() -> some View {
return self
.font(.footnote)
.foregroundColor(.gray)
.lineLimit(10)
.fixedSize(horizontal: false, vertical: true)
}
}

How to use CommonCrypto in iOS

Issue #480

Use modulemap

modulemap approach

I use modulemap in my wrapper around CommonCrypto https://github.com/onmyway133/arcane, https://github.com/onmyway133/Reindeer

For those getting header not found, please take a look https://github.com/onmyway133/Arcane/issues/4 or run xcode-select --install

  • Make a folder CCommonCrypto containing module.modulemap
module CCommonCrypto {
  header "/usr/include/CommonCrypto/CommonCrypto.h"
  export *
}
  • Go to Built Settings -> Import Paths
${SRCROOT}/Sources/CCommonCrypto

Cocoapods with modulemap approach

public header approach

Cocoapods with public header approach

1
2
s.libraries        = "xml2"
s.xcconfig = { 'HEADER_SEARCH_PATHS' => '$(SDKROOT)/usr/include/libxml2', 'OTHER_LDFLAGS' => '-lxml2' }

🐝 Interesting related posts

CommonCrypto from Xcode 10

From Xcode 10, we can just

1
import CommonCrypto

Code

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 configure test target in Xcode

Issue #478

This applies to

  • Main targets
    • App
    • Framework
  • Test targets
    • Unit tests
    • UI tests

Examples

Dependencies used

Examples

  • Cocoapods
  • Carthage

Notes

  • Make sure test target can link to all the frameworks it needs. This includes frameworks that Test targets use, and possibly frameworks that Main target uses !
  • Remember to “Clean Build Folder” and “Clear Derived Data” so that you’re sure it works. Sometimes Xcode caches.

Errors

Errors occur mostly due to linker error

  • Test target X encountered an error (Early unexpected exit, operation never finished bootstrapping - no restart will be attempted
  • Framework not found

Cocoapods

1. Pod

Test targets need to include pods that Main target uses !

or we’ll get “Framework not found”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def app_pods
pod 'Sugar', '~> 1.0'
end

def test_pods
pod 'Nimble', '~> 3.2'
pod 'Quick', '~> 0.9'
end

target 'TeaApp' do
app_pods
end

target 'TeaAppTests' do
app_pods
test_pods
end

target 'TeaAppUITests' do
app_pods
test_pods
end

Cocoapods builds a framework that contains all the frameworks the Test targets need, and configure it for us

3. Runpath Search Paths

  • Go to Test target Build Settings
  • Add $(FRAMEWORK_SEARCH_PATHS)

Carthage

1. Cartfile

We usually have

  • Cartfile for Main target
1
github "hyperoslo/Sugar" ~> 1.0
  • Cartfile.private for Test target
1
2
github "Quick/Nimble"
github "Quick/Quick"
  • Go to Test target build phase
  • Drag built frameworks from Carthage/Build
  • In rare case, we need to drag frameworks that the Main target uses
  • In rare case, we need to drag the Main target framework

3. Framework Search Paths

Configure correct path

  • Go to Test target Built Settings
  • Configure Framework Search Paths

4. Runpath Search Paths

  • Go to Test target Build Settings
  • Add $(FRAMEWORK_SEARCH_PATHS)

5. Copy Files (maybe)

From Adding frameworks to unit tests or a framework

In rare cases, you may want to also copy each dependency into the build product (e.g., to embed dependencies within the outer framework, or make sure dependencies are present in a test bundle). To do this, create a new “Copy Files” build phase with the “Frameworks” destination, then add the framework reference there as well.

Runpath Search Paths and Install name

Question

  • Why preconfigured run path “@executable_path/Frameworks” and “@loader_path/Frameworks” not work?
  • Why configuring runpath to “$(FRAMEWORK_SEARCH_PATHS)” works ?
  • Why framework has install name “@rpath/Sugar.framework/Sugar” ?

Reference

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 use react-native link and CocoaPods

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 reload data without using onAppear in SwiftUI in watchOS

Issue #468

From onAppeear

Adds an action to perform when the view appears.

In theory, this should be triggered every time this view appears. But in practice, it is only called when it is pushed on navigation stack, not when we return to it.

So if user goes to a bookmark in a bookmark list, unbookmark an item and go back to the bookmark list, onAppear is not called again and the list is not updated.

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

struct BookmarksView: View {
let service: Service
@State var items: [AnyItem]
@EnvironmentObject var storeContainer: StoreContainer

var body: some View {
List(items) { item in
makeItemRow(item: item)
.padding([.top, .bottom], 4)
}
.onAppear(perform: {
self.items = storeContainer.bookmarks(service: service).map({ AnyItem(item: $0) })
})
}
}

So instead of relying on UI state, we should rely on data state, by listening to onReceive and update our local @State

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct BookmarksView: View {
let service: Service
@State var items: [AnyItem]
@EnvironmentObject var storeContainer: StoreContainer

var body: some View {
List(items) { item in
makeItemRow(item: item)
.padding([.top, .bottom], 4)
}
.onAppear(perform: {
self.reload()
})
.onReceive(storeContainer.objectWillChange, perform: { _ in
self.reload()
})
}

private func reload() {
self.items = storeContainer.bookmarks(service: service).map({ AnyItem(item: $0) })
}
}

Inside our ObservableObject, we need to trigger changes notification

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
final class StoreContainer: ObservableObject {
let objectWillChange = PassthroughSubject<(), Never>()

func bookmark(item: ItemProtocol) {
defer {
objectWillChange.send(())
}
}

func unbookmark(item: ItemProtocol) {
defer {
objectWillChange.send(())
}
}
}

Updated at 2020-10-03 10:43:47

How to use EnvironmentObject in SwiftUI for watchOS

Issue #467

Declare top dependencies in ExtensionDelegate

1
2
3
4
5
6
7
class ExtensionDelegate: NSObject, WKExtensionDelegate {
let storeContainer = StoreContainer()

func applicationDidEnterBackground() {
storeContainer.save()
}
}

Reference that in HostingController. Note that we need to change from generic MainView to WKHostingController<AnyView> as environmentObject returns View protocol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class HostingController: WKHostingController<AnyView> {
var storeContainer: StoreContainer!

override func awake(withContext context: Any?) {
super.awake(withContext: context)
self.storeContainer = (WKExtension.shared().delegate as! ExtensionDelegate).storeContainer
}

override var body: AnyView {
return AnyView(MainView()
.environmentObject(storeContainer)
)
}
}

In theory, the environment object will be propagated down the view hierarchy, but in practice it throws error. So a workaround now is to just pass that environment object down manually

Fatal error: No ObservableObject of type SomeType found
A View.environmentObject(_:) for StoreContainer.Type may be missing as an ancestor of this 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
struct MainView: View {
@EnvironmentObject var storeContainer: StoreContainer

var body: some View {
VStack {
List(services.map({ AnyService($0) })) { anyService in
NavigationLink(destination:
ItemsView(service: anyService.service)
.navigationBarTitle(anyService.service.name)
.onDisappear(perform: {
anyService.service.requestCancellable?.cancel()
})
.environmentObject(storeContainer)
) {
HStack {
Image(anyService.service.name)
.resizable()
.frame(width: 30, height: 30, alignment: .leading)
Text(anyService.service.name)
}
}
}.listStyle(CarouselListStyle())
}
}
}

Updated at 2020-06-26 03:54:01

How to structure classes

Issue #466

iOS

1
View Controller -> View Model | Logic Handler -> Data Handler -> Repo

Android

1
Activity -> Fragment -> View Model | Logic Handler -> Data Handler -> Repo

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 deal with multiple scenarios with Push Notification in iOS

Issue #459

Here are my notes when working with Push Notification

How to register

Register to receive push notification

registerForRemoteNotificationTypes is deprecated in iOS 8+

1
UIApplication.sharedApplication().registerForRemoteNotifications()

Register to alert user through UI

If your app displays alerts, play sounds, or badges its icon, you must call this method during your launch cycle to request permission to alert the user in these ways

1
2
3
4
5
let types: UIUserNotificationType = [.Badge, .Sound, .Alert]
let categories = Set<UIUserNotificationCategory>()
let settings = UIUserNotificationSettings(forTypes: types, categories: categories)

UIApplication.sharedApplication().registerUserNotificationSettings(settings)

You don’t need to wait for registerUserNotificationSettings to callback before calling registerForRemoteNotifications

From iOS 10, use UNNotifications framework https://onmyway133.github.io/blog/How-to-register-for-alert-push-notification-in-iOS/

When to register

From Registering, Scheduling, and Handling User Notifications

Never cache a device token; always get the token from the system whenever you need it. If your app previously registered for remote notifications, calling the registerForRemoteNotifications method again does not incur any additional overhead, and iOS returns the existing device token to your app delegate immediately. In addition, iOS calls your delegate method any time the device token changes, not just in response to your app registering or re-registering
The user can change the notification settings for your app at any time using the Settings app. Because settings can change, always call the registerUserNotificationSettings: at launch time and use the application:didRegisterUserNotificationSettings: method to get the response. If the user disallows specific notification types, avoid using those types when configuring local and remote notifications for your app.

didReceiveRemoteNotification

About application:didReceiveRemoteNotification:

Implement the application:didReceiveRemoteNotification:fetchCompletionHandler: method instead of this one whenever possible. If your delegate implements both methods, the app object calls the application:didReceiveRemoteNotification:fetchCompletionHandler: method.
If the app is not running when a remote notification arrives, the method launches the app and provides the appropriate information in the launch options dictionary. The app does not call this method to handle that remote notification. Instead, your implementation of the application:willFinishLaunchingWithOptions: or application:didFinishLaunchingWithOptions: method needs to get the remote notification payload data and respond appropriately.

About application:didReceiveRemoteNotification:fetchCompletionHandler:

This is for silent push notification with content-available

Unlike the application:didReceiveRemoteNotification: method, which is called only when your app is running in the foreground, the system calls this method when your app is running in the foreground or background
In addition, if you enabled the remote notifications background mode, the system launches your app (or wakes it from the suspended state) and puts it in the background state when a push notification arrives. However, the system does not automatically launch your app if the user has force-quit it. In that situation, the user must relaunch your app or restart the device before the system attempts to launch your app automatically again.
If the user opens your app from the system-displayed alert, the system may call this method again when your app is about to enter the foreground so that you can update your user interface and display information pertaining to the notification.

How to handle

Usually, the use of push notification is to display a specific article, a specific DetailViewControllerin your app. So the good practices are

  • When the app is in foreground: Gently display some kind of alert view and ask the user whether he would like to go to that specific page or not

  • When user is brought from background to foreground, or from terminated to foreground: Just navigate to that specific page. For example, if you use UINavigationController, you can set that specific page the top most ViewController, if you use UITabBarController, you can set that specific page the selected tab, something like that

1
2
3
4
5
6
7
8
9
10
11
- func handlePushNotification(userInfo: NSDictionary) {
// Check applicationState
if (applicationState == UIApplicationStateActive) {
// Application is running in foreground
showAlertForPushNotification(userInfo)
}
else if (applicationState == UIApplicationStateBackground || applicationState == UIApplicationStateInactive) {
// Application is brought from background or launched after terminated
handlePushNotification(userInfo)
}
}

Here we create another method handlePushNotification to handle push notification. When you receive push notification, 3 cases can occur

Case 1: Foreground

Loud push

  • No system alert

  • application:didReceiveRemoteNotification:fetchCompletionHandler: is called

Silent push

  • No system alert

  • application:didReceiveRemoteNotification:fetchCompletionHandler: is called

Case 2: Background

Loud push

  • System alert

  • No method called

  • Tap notification and application:didReceiveRemoteNotification:fetchCompletionHandler: is called

  • Tap on App Icon and nothing is called

Silent push

  • No system alert

  • application:didReceiveRemoteNotification:fetchCompletionHandler: is called. If app is suspended, its state changed to UIApplicationStateBackground

  • Tap notification and application:didReceiveRemoteNotification:fetchCompletionHandler: is called

  • Tap on App Icon and nothing is called

Case 3: Terminated

Loud push

  • System alert

  • No method called

  • Tap notification and application:didFinishLaunchingWithOptions: with launchOptions, application:didReceiveRemoteNotification:fetchCompletionHandler: is called

  • Tap on App Icon and application:didFinishLaunchingWithOptions: is called with launchOptions set to nil

Silent push

  • No system alert

  • application:didReceiveRemoteNotification:fetchCompletionHandler: is called. If app was not killed by user, it is woke up and state changed to UIApplicationStateInactive.

  • Tap notification and application:didFinishLaunchingWithOptions: with launchOptions, application:didReceiveRemoteNotification:fetchCompletionHandler: is called

  • Tap on App Icon and application:didFinishLaunchingWithOptions: is called with launchOptions set to nil

System alert

System alert only show if the payload contains alert

1
2
3
4
5
6
7
8
9
10
11
12
{
"aps" : {
"alert" : {
"title" : "Game Request",
"body" : "Bob wants to play poker",
"action-loc-key" : "PLAY"
},
"badge" : 5
},
"param1" : "bar",
"param2" : [ "bang", "whiz" ]
}

Silent push payload

For now I see that silent push must contain sound for application:didReceiveRemoteNotification:fetchCompletionHandler: to be called when app is in background

1
2
3
4
5
6
7
8
9
{
“aps”: {
“content-available”: 1,
“alert”: “hello” // include this if we want to show alert
“sound”: “” // this does the trick
},
“param1”: 1,
“param2”: “text”
}

Read Pushing Background Updates to Your App

If your app’s server-based content changes infrequently or at irregular intervals, you can use background notifications to notify your app when new content becomes available. A background notification is a remote notification that doesn’t display an alert, play a sound, or badge your app’s icon. It wakes your app in the background and gives it time to initiate downloads from your server and update its content.

The system treats background notifications as low-priority: you can use them to refresh your app’s content, but the system doesn’t guarantee their delivery. In addition, the system may throttle the delivery of background notifications if the total number becomes excessive. The number of background notifications allowed by the system depends on current conditions, but don’t try to send more than two or three per hour.

How to test Push notification

I built a macOS app called PushNotification for you to test push notification. It works with certificate and the new key authentication with APNS. Please give it a try

Tutorials that use PushNotifications

Troubleshooting

Silent notification with push notification enabled

In theory, if user disables push notification then silent notification still goes through

https://stackoverflow.com/questions/31450403/didreceiveremotenotification-not-working-in-the-background

but sound key should be present

1
2
3
4
5
6
{
aps = {
"content-available" : 1,
sound : ""
};
}

When open the app, didReceiveRemoteNotification is called immediately with the silent push message

Where to go from here

I hope you find this article useful. iOS changes fast so some the things I mention may be outdated by the time you read, if so please let me know. Here are some more interesting links


Original post https://medium.com/fantageek/push-notification-in-ios-46d979e5f7ec

How to use Apple certificate in Xcode 11

Issue #458

For push notification, we can now use just Production Certificate for 2 environments (production and sandbox) instead of Development and Production certificates.

Now for code signing, with Xcode 11 https://developer.apple.com/documentation/xcode_release_notes/xcode_11_release_notes we can just use Apple Development and Apple Distribution certificate for multiple platforms

Signing and capabilities settings are now combined within a new Signing & Capabilities tab in the Project Editor. The new tab enables using different app capabilities across multiple build configurations. The new capabilities library makes it possible to search for available capabilities

Xcode 11 supports the new Apple Development and Apple Distribution certificate types. These certificates support building, running, and distributing apps on any Apple platform. Preexisting iOS and macOS development and distribution certificates continue to work, however, new certificates you create in Xcode 11 use the new types. Previous versions of Xcode don’t support these certificates

How to create watch only watchOS app

Issue #457

From Creating Independent watchOS Apps

The root target is a stub, and acts as a wrapper for your project, so that you can submit it to the App Store. The other two are identical to the targets found in a traditional watchOS project. They represent your WatchKit app and WatchKit extension, respectively.

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 UserNotificationsUI in iOS

Issue #454

From documentation https://developer.apple.com/documentation/usernotificationsui

Customize how local and remote notifications appear on the user’s device by adding a notification content app extension to the bundle of your iOS app. Your extension manages a custom view controller, which you use to present the content from incoming notifications. When a notification arrives, the system displays your view controller in addition to, or in place of, the default system interface.

https://developer.apple.com/documentation/usernotificationsui/customizing_the_appearance_of_notifications

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

Use Notification Content Extension

1
2
3
4
func didReceive(_ notification: UNNotification) {
self.bodyText?.text = notification.request.content.body
self.headlineText?.text = notification.request.content.title
}