How to handle radio group for NSButton

Issue #579

Use same action, or we can roll our own implementation

An NSButton configured as a radio button (with the -buttonType set to NSRadioButton), will now operate in a radio button group for applications linked on 10.8 and later. To have the button work in a radio group, use the same -action for each NSButton instance, and have the same superview for each button. When these conditions are met, checking one button (by changing the -state to 1), will uncheck all other buttons (by setting their -state to 0).

1
2
3
4
5
6
7
import Omnia

@IBAction func onModeButtonTouch(_ sender: NSRadioButton) {
for button in [mode1Button, mode2Button] {
button?.isOn = button === sender
}
}

How to specify locale in Swift

Issue #578

Read Language and Locale IDs

1
2
zh-Hans_HK
[language designator]-[script designator]_[region designator]

Language IDs

A language ID identifies a language used in many regions, a dialect used in a specific region, or a script used in multiple regions. To specify a language used in many regions, use a language designator by itself. To specify a specific dialect, use a hyphen to combine a language designator with a region designator. To specify a script, combine a language designator with a script designator. For example, to specify common English, use the en language designator as the language ID. To specify the English language as it is used in the United Kingdom, use en-GB as the language ID.

Locale IDs

A locale ID identifies a specific region and its cultural conventions—such as the formatting of dates, times, and numbers. To specify a locale, use an underscore character to combine a language ID with a region designator, as shown in Table B-5. For example, the locale ID for English-language speakers in the United Kingdom is en_GB, while the locale for English-speaking residents of the United States is en_US.

Read more

How to workaround URLSession issue in watchOS 6.1.1

Issue #577

https://stackoverflow.com/questions/59724731/class-avassetdownloadtask-is-implemented-in-both-cfnetwork-and-avfoundation

1
2
3
objc[45250]: Class AVAssetDownloadTask is implemented in both /Applications/Xcode.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/CFNetwork.framework/CFNetwork (0x4ddd0ec) and /Applications/Xcode.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/AVFoundation.framework/AVFoundation (0x16aea494). One of the two will be used. Which one is undefined.

objc[45250]: Class AVAssetDownloadURLSession is implemented in both /Applications/Xcode.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/CFNetwork.framework/CFNetwork (0x4dddd44) and /Applications/Xcode.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/AVFoundation.framework/AVFoundation (0x16aea4bc). One of the two will be used. Which one is undefined.

Then URLSession stops working.

1
2020-01-13 22:50:12.430920+0100 MyAppWatch WatchKit Extension[45250:2099229] Task <3CECDE81-59B9-4EDE-A4ED-1BA173646037>.<1> finished with error [-999] Error Domain=NSURLErrorDomain Code=-999 "cancelled" UserInfo={NSErrorFailingURLKey=https://myapp.com/def.json, NSErrorFailingURLStringKey=https://myapp.com/def.json, NSLocalizedDescription=cancelled}

The workaround is to remove Combine based API, and use completion block.

Instead of dataTaskPublisher which hangs indefinitely, no sink is reported

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
URLSession.shared
.dataTaskPublisher(for: url)
.map({ $0.data })
.decode(type: T.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
.receive(on: RunLoop.main)
.sink(receiveCompletion: { completionStatus in
switch completionStatus {
case .finished:
break
case .failure(let error):
completion(.failure(error))
}
}, receiveValue: { value in
completion(.success(value))
})

just use normal

1
2
3
4
5
6
7
8
9
let task = URLSession.shared.dataTask(with: url, completionHandler: { data, response, error in
if let data = data, let model = try? JSONDecoder().decode(T.self, from: data) {
completion(.success(model))
} else {
completion(.failure(error ?? ServiceError.noInternet))
}
})

task.resume()

How to generate XCTest test methods

Issue #576

Code

See Spek

Override testInvocations to specify test methods

https://developer.apple.com/documentation/xctest/xctestcase/1496271-testinvocations

Returns an array of invocations representing each test method in the test case.

Because testInvocations is unavailable in Swift, we need to use ObjC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#import "include/SpekHelperTestCase.h"

@implementation SpekHelperTestCase

- (instancetype)init {
self = [super initWithInvocation: nil];
return self;
}

+ (NSArray<NSInvocation *> *)testInvocations {
NSArray<NSString *> *selectorStrings = [self spekGenerateTestMethodNames];
NSMutableArray<NSInvocation *> *invocations = [NSMutableArray arrayWithCapacity:selectorStrings.count];

for (NSString *selectorString in selectorStrings) {
SEL selector = NSSelectorFromString(selectorString);
NSMethodSignature *signature = [self instanceMethodSignatureForSelector:selector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.selector = selector;

[invocations addObject:invocation];
}

return invocations;
}

+ (NSArray<NSString *> *)spekGenerateTestMethodNames {
return @[];
}

@end

Generate test methods

Calculate based on Describe and It, and use Objc runtime class_addMethod to add instance methods

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
open class SpekTestCase: SpekHelperTestCase {
open class func makeDescribe() -> Describe {
return Describe("empty")
}

#if canImport(SpekHelper)

override public class func spekGenerateTestMethodNames() -> [String] {
let describe = Self.makeDescribe()

var names: [String] = []
generate(describe: describe, names: &names)
return names
}

private static func addInstanceMethod(name: String, closure: @escaping () -> Void) -> String {
let block: @convention(block) (SpekTestCase) -> Void = { spekTestCase in
let _ = spekTestCase
closure()
}

let implementation = imp_implementationWithBlock(block as Any)
let selector = NSSelectorFromString(name)
class_addMethod(self, selector, implementation, "v@:")

return name
}
}

Read more

How to use ObjC in Swift Package Manager

Issue #575

Create Objc target

Check runtime

Check for example _runtime(_ObjC) or os(macOS if you plan to use platform specific feature

For example, in test we use XCTest which is run via Xcode and is a macOS framework, so we need to check for os(macOS)

Note that in Objc framework, the header files must be in include folder

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
targets: {
var targets: [Target] = [
.testTarget(
name: "QuickTests",
dependencies: [ "Quick", "Nimble" ],
exclude: [
"QuickAfterSuiteTests/AfterSuiteTests+ObjC.m",
"QuickFocusedTests/FocusedTests+ObjC.m",
"QuickTests/FunctionalTests/ObjC",
"QuickTests/Helpers/QCKSpecRunner.h",
"QuickTests/Helpers/QCKSpecRunner.m",
"QuickTests/Helpers/QuickTestsBridgingHeader.h",
"QuickTests/QuickConfigurationTests.m",
]
),
]
#if os(macOS)
targets.append(contentsOf: [
.target(name: "QuickSpecBase", dependencies: []),
.target(name: "Quick", dependencies: [ "QuickSpecBase" ]),
])
#else
targets.append(contentsOf: [
.target(name: "Quick", dependencies: []),
])
#endif
return targets
}(),

How to fix library not found with SPM and CocoaPods in Xcode

Issue #572

After migrating a few pods to use SPM, some libraries fail to load. This is because the workspace now uses both SPM and cocoapods

code signature in … not valid for use in process using Library Validation: mapped file has no Team ID and is not a platform binary (signed with custom identity or adhoc?)

The workaround is to disable Library validation

Screenshot 2020-01-08 at 22 48 28

How to get updated safeAreaInsets in iOS

Issue #570

Use viewSafeArea

1
2
3
4
5
6
@available(iOS 11.0, *)
override func viewSafeAreaInsetsDidChange() {
super.viewSafeAreaInsetsDidChange()

self.collectionView.reloadData()
}

Use https://developer.apple.com/documentation/uikit/uiview/2891102-safearealayoutguide

1
view.safeAreaLayoutGuide.layoutFrame

https://developer.apple.com/documentation/uikit/uiview/2891103-safeareainsets

For the view controller’s root view, the insets account for the status bar, other visible bars, and any additional insets that you specified using the additionalSafeAreaInsets property of your view controller. For other views in the view hierarchy, the insets reflect only the portion of the view that is covered. For example, if a view is entirely within the safe area of its superview, the edge insets in this property are 0.

Use UICollectionView.contentInsetAdjustmentBehavior

Nested view

For UICollectionView inside Cell inside UICollectionView, its insets is 0, but its parent parent is correct, which is the original cell

UICollectionView -> Cell -> ContentView -> UICollectionView

1
collectionView.superview?.superview?.safeAreaInsets

viewWillAppear: safeAreaInsets is not set to collectionView
viewDidAppear: safeAreaInsets is set to collectionView and cells, but not to nested collectionView

In viewSafeAreaInsetsDidChange, invalidate outer and nested collectionViewLayout

Use extendedLayoutIncludesOpaqueBars

https://developer.apple.com/documentation/uikit/uiviewcontroller/1621404-extendedlayoutincludesopaquebars

A Boolean value indicating whether or not the extended layout includes opaque bars.

Seems to affect left and right insets

But it’s not, it is because of when safeAreaInsets is available, and how it is passed to nested view

When invalidating collection view layout with custom UIPresentationController, alongsideTransition is called twice, the first time with old safeAreaInsets, and the second time with latest safeAreaInsets

And the layout invalidation uses the old insets.

1
2
3
4
5
6
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { _ in
self.collectionViewLayout.invalidateLayout()
})
}

Dispatch

1
2
3
4
5
6
7
8
9
// UIViewController subclass
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { _ in
DispatchQueue.main.async {
self.collectionViewLayout.invalidateLayout()
}
})
}

Call layoutIfNeeded

1
2
3
4
5
6
7
8
// UIPresentationController subclass
override var frameOfPresentedViewInContainerView: CGRect {
guard let containerView = containerView else { return .zero }

presentedView?.setNeedsLayout()
presentedView?.layoutIfNeeded()

return ...

Check

Check that UICollectionView or the view you’re working on is in view hierarchy

Check that you’re using code in viewDidLayoutSubviews when safeAreaInsets is known

Read more

How to disable implicit decoration view animation in UICollectionView

Issue #569

From documentation https://developer.apple.com/documentation/uikit/uicollectionviewlayout/1617726-initiallayoutattributesforappear

This method is called after the prepare(forCollectionViewUpdates:) method and before the finalizeCollectionViewUpdates() method for any decoration views that are about to be inserted. Your implementation should return the layout information that describes the initial position and state of the view. The collection view uses this information as the starting point for any animations. (The end point of the animation is the view’s new location in the collection view.) If you return nil, the layout object uses the item’s final attributes for both the start and end points of the animation.

The default implementation of this method returns nil.

Although the doc says “The default implementation of this method returns nil”, calling super.initialLayoutAttributesForAppearingDecorationElement gives somehow implicit animation. The workaround is to explicitly return nil

1
2
3
4
5
6
7
func initialLayoutAttributesForAppearingDecorationElement(ofKind elementKind: String, at decorationIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return nil
}

func finalLayoutAttributesForDisappearingDecorationElement(ofKind elementKind: String, at decorationIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return nil
}

Decoration seems to be removed when all items are removed. Workaround is to check and only add decoration when there is preferred data or cell

How to make simple tracker via swizzling in Swift

Issue #568

Code

Swizzle viewDidAppear

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

var mapping: [String: (UIViewController) -> Void] = [:]
var hasSwizzled = false

public func track<T: UIViewController>(_ type: T.Type, block: @escaping (T) -> Void) {
let original = #selector(UIViewController.viewDidAppear(_:))
let swizled = #selector(UIViewController.trackers_viewDidAppear(_:))

if !hasSwizzled {
swizzle(kClass: UIViewController.self, originalSelector: original, swizzledSelector: swizled)
hasSwizzled = true
}

mapping[NSStringFromClass(type)] = { controller in
if let controller = controller as? T {
block(controller)
}
}
}

extension UIViewController {
func trackers_viewDidAppear(_ animated: Bool) {
trackers_viewDidAppear(animated)

let string = NSStringFromClass(type(of: self))
mapping[string]?(self)
}
}

func swizzle(kClass: AnyClass, originalSelector: Selector, swizzledSelector: Selector) {
let originalMethod = class_getInstanceMethod(kClass, originalSelector)
let swizzledMethod = class_getInstanceMethod(kClass, swizzledSelector)

let didAddMethod = class_addMethod(kClass, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))

if didAddMethod {
class_replaceMethod(kClass, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
} else {
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}

Track in a declarative way

1
2
3
4
5
6
7
8
9
10
11
track(ListController.self) {
print("list controller has appeared")
}

track(DetailController.self) {
print("detail controller has appeared")
}

track(CouponController.self) { controller in
print("coupon controller has appeared with code \(controller.coupon.code)")
}

How to make simple adapter for delegate and datasource for UICollectionView and UITableView

Issue #567

Code

Make open Adapter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
import UIKit

public protocol AdapterDelegate: class {

/// Apply model to view
func configure(model: Any, view: UIView, indexPath: IndexPath)

/// Handle view selection
func select(model: Any)

/// Size the view
func size(model: Any, containerSize: CGSize) -> CGSize
}

/// Act as DataSource and Delegate for UICollectionView, UITableView
open class Adapter: NSObject,
UICollectionViewDataSource, UICollectionViewDelegateFlowLayout,
UITableViewDataSource, UITableViewDelegate {

public var sections: [Section] = []
public weak var collectionView: UICollectionView?
public weak var tableView: UITableView?
public weak var delegate: AdapterDelegate?

let registryService = RegistryService()

// MARK: - Initialiser
public required init(collectionView: UICollectionView) {
self.collectionView = collectionView
super.init()
}

public required init(tableView: UITableView) {
self.tableView = tableView
super.init()
}

// MARK: - UICollectionViewDataSource
open func numberOfSections(in collectionView: UICollectionView) -> Int {
return sections.count
}

open func collectionView(_ collectionView: UICollectionView,
numberOfItemsInSection section: Int) -> Int {
return sections[section].items.count
}

open func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let item = sections[indexPath.section].items[indexPath.row]
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: item.cellType.typeName,
for: indexPath)

delegate?.configure(model: item.model, view: cell, indexPath: indexPath)

return cell
}

open func collectionView(
_ collectionView: UICollectionView,
viewForSupplementaryElementOfKind kind: String,
at indexPath: IndexPath) -> UICollectionReusableView {

if let header = sections[indexPath.section].header,
kind == UICollectionElementKindSectionHeader {

let view = collectionView.dequeueReusableSupplementaryView(
ofKind: UICollectionElementKindSectionHeader,
withReuseIdentifier: header.viewType.typeName,
for: indexPath
)

delegate?.configure(model: header.model, view: view, indexPath: indexPath)
return view
} else if let footer = sections[indexPath.section].footer,
kind == UICollectionElementKindSectionFooter {

let view = collectionView.dequeueReusableSupplementaryView(
ofKind: UICollectionElementKindSectionFooter,
withReuseIdentifier: footer.viewType.typeName,
for: indexPath
)

delegate?.configure(model: footer.model, view: view, indexPath: indexPath)
return view
} else {
let view = DummyReusableView()
view.isHidden = true
return view
}
}

// MARK: - UICollectionViewDelegate
open func collectionView(
_ collectionView: UICollectionView,
didSelectItemAt indexPath: IndexPath) {

let item = sections[indexPath.section].items[indexPath.row]
delegate?.select(model: item.model)
collectionView.deselectItem(at: indexPath, animated: true)
}

open func collectionView(
_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {

let item = sections[indexPath.section].items[indexPath.row]
if let size = delegate?.size(model: item.model, containerSize: collectionView.frame.size) {
return size
}

if let size = (collectionViewLayout as? UICollectionViewFlowLayout)?.itemSize {
return size
}

return collectionView.frame.size
}

open func collectionView(
_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
referenceSizeForHeaderInSection section: Int) -> CGSize {

guard let header = sections[section].header else {
return .zero
}

guard let size = delegate?.size(model: header.model, containerSize: collectionView.frame.size) else {
return .zero
}

return size
}

open func collectionView(
_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
referenceSizeForFooterInSection section: Int) -> CGSize {

guard let footer = sections[section].footer else {
return .zero
}

guard let size = delegate?.size(model: footer.model, containerSize: collectionView.frame.size) else {
return .zero
}

return size
}

// MARK: - Reload
open func reload(sections: [Section]) {
// Registry
registryService.registerIfNeeded(
collectionView: collectionView,
tableView: tableView,
sections: sections
)

self.sections = sections
collectionView?.reloadData()
tableView?.reloadData()
}

// MARK: - UITableViewDataSource
open func numberOfSections(in tableView: UITableView) -> Int {
return sections.count
}

open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return sections[section].items.count
}

open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let item = sections[indexPath.section].items[indexPath.row]
let cell = tableView.dequeueReusableCell(
withIdentifier: item.cellType.typeName,
for: indexPath
)

delegate?.configure(model: item.model, view: cell, indexPath: indexPath)

return cell
}

// MARK: - UITableViewDelegate
open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let item = sections[indexPath.section].items[indexPath.row]
delegate?.select(model: item.model)
tableView.deselectRow(at: indexPath, animated: true)
}

open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let item = sections[indexPath.section].items[indexPath.row]
if let size = delegate?.size(model: item.model, containerSize: tableView.frame.size) {
return size.height
}

return 0
}

open func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
guard let header = sections[section].header else {
return 0
}

guard let size = delegate?.size(model: header.model, containerSize: tableView.frame.size) else {
return 0
}

return size.height
}

open func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
guard let footer = sections[section].footer else {
return 0
}

guard let size = delegate?.size(model: footer.model, containerSize: tableView.frame.size) else {
return 0
}

return size.height
}

open func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
guard let header = sections[section].header else {
return nil
}

guard let view = tableView.dequeueReusableHeaderFooterView(withIdentifier: header.viewType.typeName) else {
return nil
}

delegate?.configure(model: header.model, view: view, indexPath: IndexPath(row: 0, section: section))
return view
}

open func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
guard let footer = sections[section].footer else {
return nil
}

guard let view = tableView.dequeueReusableHeaderFooterView(withIdentifier: footer.viewType.typeName) else {
return nil
}

delegate?.configure(model: footer.model, view: view, indexPath: IndexPath(row: 0, section: section))
return view
}
}

Declare data

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let sections: [Section] = [
Section(
header: Header(model: Model.header("Information"), viewType: HeaderView.self),
items: [
Item(model: Model.avatar(avatarUrl), cellType: AvatarCell.self),
Item(model: Model.name("Thor"), cellType: NameCell.self),
Item(model: Model.location("Asgard"), cellType: NameCell.self)
]
),
Section(
header: Header(model: Model.header("Skills"), viewType: HeaderView.self),
items: [
Item(model: Model.skill("iOS"), cellType: SkillCell.self),
Item(model: Model.skill("Android"), cellType: SkillCell.self)
]
)
]

adapter.reload(sections: sections)

Configure required blocks

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
extension ProfileViewController: AdapterDelegate {
func configure(model: Any, view: UIView, indexPath: IndexPath) {
guard let model = model as? Model else {
return
}

switch (model, view) {
case (.avatar(let string), let cell as Avatarcell):
cell.configure(string: string)
case (.name(let name), let cell as NameCell):
cell.configure(string: name)
case (.header(let string), let view as HeaderView):
view.configure(string: string)
default:
break
}
}

func select(model: Any) {
guard let model = model as? Model else {
return
}

switch model {
case .skill(let skill):
let skillController = SkillController(skill: skill)
navigationController?.pushViewController(skillController, animated: true)
default:
break
}
}

func size(model: Any, containerSize: CGSize) -> CGSize {
guard let model = model as? Model else {
return .zero
}

switch model {
case .name:
return CGSize(width: containerSize.width, height: 40)
case .avatar:
return CGSize(width: containerSize.width, height: 200)
case .header:
return CGSize(width: containerSize.width, height: 30)
default:
return .zero
}
}
}

Extending Manager

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class AccordionManager<T>: Manager<T> {
private var collapsedSections = Set<Int>()

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return collapsedSections.contains(section)
? 0 : sections[section].items.count
}

func toggle(section: Int) {
if collapsedSections.contains(section) {
collapsedSections.remove(section)
} else {
collapsedSections.insert(section)
}

let indexSet = IndexSet(integer: section)
tableView?.reloadSections(indexSet, with: .automatic)
}
}

How to make progress HUD in Swift

Issue #566

Code

Create a container that has blur effect

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
public class HUDContainer: UIVisualEffectView, AnimationAware {
private let innerContentView: UIView & AnimationAware
public let label = UILabel()
public var text: String? {
didSet {
label.text = text
label.sizeToFit()
label.isHidden = text == nil
}
}

public init(contentView: UIView & AnimationAware) {
self.innerContentView = contentView
super.init(effect: UIBlurEffect(style: .light))
self.contentView.addSubview(innerContentView)
self.contentView.addSubview(label)

innerContentView.pinEdgesToSuperview()
configure()
}

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

public func configure() {
layer.cornerRadius = 8
layer.masksToBounds = true

label.isHidden = false
label.font = UIFont.preferredFont(forTextStyle: .body)
label.textColor = UIColor.black
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false

NSLayoutConstraint.activate([
label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8),
label.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 10),
label.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -10),
])
}

public func startAnimation() {
innerContentView.startAnimation()
}

public func endAnimation() {
innerContentView.stopAnimation()
}
}

Make error view with 2 cross lines

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

public class ErrorView: UIView, AnimationAware {
public let line1 = CAShapeLayer()
public let line2 = CAShapeLayer()

public let animation1 = CASpringAnimation(keyPath: #keyPath(CALayer.transform))
public let animation2 = CASpringAnimation(keyPath: #keyPath(CALayer.transform))

public var lineColor: UIColor = UIColor.darkGray
public var duration: TimeInterval = 0.75

public override init(frame: CGRect) {
super.init(frame: frame)
layer.addSublayer(line1)
layer.addSublayer(line2)
configure()
}

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

public override func layoutSubviews() {
super.layoutSubviews()

configureSize()
}

public func configure() {
[line1, line2].forEach {
$0.backgroundColor = lineColor.cgColor
}

[animation1, animation2].forEach {
$0.fromValue = 0
$0.damping = 0.33
$0.initialVelocity = 0.01
$0.mass = 0.2
$0.duration = duration
$0.valueFunction = CAValueFunction(name: CAValueFunctionName.rotateZ)
$0.timingFunction = CAMediaTimingFunction(name: .easeIn)
}

animation1.toValue = CGFloat.pi / 4
animation2.toValue = -CGFloat.pi / 4
}

private func configureSize() {
guard line1.frame.width <= 0 else {
return
}

[line1, line2].forEach {
$0.cornerRadius = 3
$0.frame.size = CGSize(width: bounds.width*0.6, height: 6)
$0.position = layer.position
}
}

public override func didMoveToWindow() {
super.didMoveToWindow()

line1.transform = CATransform3DIdentity
line2.transform = CATransform3DIdentity
}

public func startAnimation() {
line1.transform = CATransform3DMakeRotation(CGFloat.pi/4, 0, 0, 1.0)
line2.transform = CATransform3DMakeRotation(-CGFloat.pi/4, 0, 0, 1.0)

line1.add(animation1, forKey: nil)
line2.add(animation2, forKey: nil)
}

public func stopAnimation() {
line1.removeAllAnimations()
line2.removeAllAnimations()
}
}

Make loading progress using replicator layers

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

public class ProgressView: UIView, AnimationAware {
public let replicatorLayer = CAReplicatorLayer()
public let animation = CABasicAnimation(keyPath: #keyPath(CALayer.opacity))
public let line = CALayer()

public var lineCount: Int = 12
public var duration: TimeInterval = 1.0
public var lineSize: CGSize = CGSize(width: 20, height: 6)
public var lineColor: UIColor = UIColor.darkGray

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

configure()
}

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

public func configure() {
let angle = CGFloat.pi * 2 / CGFloat(lineCount)
let rotation = CATransform3DMakeRotation(angle, 0, 0, 1.0)

replicatorLayer.instanceTransform = rotation
replicatorLayer.instanceCount = lineCount
replicatorLayer.instanceDelay = duration / TimeInterval(lineCount)

line.backgroundColor = lineColor.cgColor
line.frame.size = lineSize
line.cornerRadius = lineSize.height / 2

animation.fromValue = 1.0
animation.toValue = 0.0
animation.repeatCount = Float.greatestFiniteMagnitude
animation.timingFunction = CAMediaTimingFunction(name: .linear)
animation.duration = duration

replicatorLayer.addSublayer(line)
layer.addSublayer(replicatorLayer)

// x: the larger, the closer to center
// y: half the height, changing affects rotation of lines
line.position = CGPoint(x: 48, y: 75)
}

public override func layoutSubviews() {
super.layoutSubviews()

replicatorLayer.frame = bounds
}

public func startAnimation() {
line.add(animation, forKey: nil)
}

public func stopAnimation() {
line.removeAllAnimations()
}
}

Make success view with check mark animation

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

public class SuccessView: UIView, AnimationAware {
public let shapeLayer = CAShapeLayer()
public let animation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.strokeEnd))

public var lineColor: UIColor = UIColor.darkGray
public var duration: TimeInterval = 0.25

public override init(frame: CGRect) {
super.init(frame: frame)
layer.addSublayer(shapeLayer)
configure()
}

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

public override func layoutSubviews() {
super.layoutSubviews()

configurePath()
}

public func configure() {
shapeLayer.lineCap = .round
shapeLayer.lineJoin = .round
shapeLayer.fillColor = nil
shapeLayer.strokeColor = lineColor.cgColor
shapeLayer.lineWidth = 6

animation.timingFunction = CAMediaTimingFunction(name: .easeIn)
animation.fromValue = 0.0
animation.toValue = 1.0
animation.duration = duration
}

private func configurePath() {
let size = CGSize(width: 80, height: 60)
shapeLayer.frame = CGRect(origin: .zero, size: size)
shapeLayer.position = layer.position

let path = UIBezierPath()
path.move(to: CGPoint(x: size.width * 0, y: size.height * 0.48))
path.addLine(to: CGPoint(x: size.width * 0.38, y: size.height))
path.addLine(to: CGPoint(x: size.width, y: size.height * 0.01))

shapeLayer.path = path.cgPath
}

public override func didMoveToWindow() {
super.didMoveToWindow()

shapeLayer.strokeEnd = 0.0
}

public func startAnimation() {
shapeLayer.strokeEnd = 1.0
shapeLayer.add(animation, forKey: nil)
}

public func stopAnimation() {
shapeLayer.removeAllAnimations()
}
}

How to build static site using Publish

Issue #564

Code

Steps

Step 1: Create executable

1
swift package init --type executable

Step 2: Edit package

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// swift-tools-version:5.1
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription

let package = Package(
name: "PumaSwiftWeb",
dependencies: [
.package(url: "https://github.com/johnsundell/publish.git", from: "0.1.0")
],
targets: [
.target(
name: "PumaSwiftWeb",
dependencies: [
"Publish"
]
)
]
)

Step 3: Double click Package.swift, Xcode opens that in a generated project
Step 4: Declare website. Go to Sources/main.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 Publish
import Plot
import Ink
import Foundation

struct PumaSwiftWeb: Website {
enum SectionID: String, WebsiteSectionID {
case gettingStarted
case workflow
case tasks
case about
}

struct ItemMetadata: WebsiteItemMetadata {}

var url = URL(string: "https://pumaswift.github.io")!
var name = "Puma Swift"
var description = "Build utilities in pure Swift"
var language: Language { .english }
var imagePath: Path? { "https://avatars2.githubusercontent.com/u/54233247?s=200&v=4" }
}

try PumaSwiftWeb()
.publish(withTheme: .foundation)

Step 5: Create Content folder
Step 6: swift run
Step 7: Copy Output to root and push to GitHub

How to use iTMSTransporter

Issue #563

Transporter app

The new Transporter app for macOS makes it easy to upload your binary to App Store Connect. To get started, download Transporter from the Mac App Store, and simply drag and drop your binaries into the upload window. With Transporter you can

https://stackoverflow.com/questions/8094317/where-to-find-application-loader-app-in-mac/40419328

1
2
3
4
5
6
7
8
As of Xcode 11, "Application Loader is no longer included with Xcode", per the Xcode 11 Release Notes:

Xcode supports uploading apps from the Organizer window or from the command line with xcodebuild or xcrun altool. Application Loader is no longer included with Xcode. (29008875)

The Xcode Help page, Upload an app to App Store Connect, explains how to upload from the Xcode Archives Organizer.

Transporter
In October 2019, Apple announced the Transporter app for macOS, now available in the Mac App Store.

Find path

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
public struct Transporter {
var arguments: [String] = []

func run(workflow: Workflow) throws {
guard Folder.directoryExists(path: "/Applications/Transporter.app") else {
Deps.console.warn("You need to install Transporter")
throw PumaError.invalid
}

try CommandLine().runBash(
workflow: workflow,
program: transportPath(),
arguments: arguments
)
}

private func transportPath() throws -> String {
if Folder.directoryExists(path: "/Applications/Transporter.app") {
return "/Applications/Transporter.app/Contents/itms/bin/iTMSTransporter"
} else if Folder.directoryExists(path: "/Applications/Xcode.app/Contents/Applications/Application Loader.app/") {
return "/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/MacOS/itms/bin/iTMSTransporter"
} else {
throw PumaError.invalid
}
}
}

Help

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
usage: iTMSTransporter [-help <arg> | -info | -m <arg> | -version]   [-o <arg>] [-v
<arg>] [-WONoPause <arg>] [-Xmx4096m]
iTMSTransporter : iTunes Store Transporter 2.0.0
-help <arg> Show this help. If a mode value is specified, show help specific
to that mode.
-info The -info option should be used by itself and returns the
copyright notice and acknowledgements.
-m <arg> The -m option specifies the tool's mode. The valid values are:
verify, upload, provider, diagnostic, lookupMetadata,
createArtist, lookupArtist, status, statusAll,
createMetadataTicket, queryTickets, generateSchema, transferTest,
downloadMetadataGuides, listReports, requestReport
-o <arg> The -o option specifies the directory and filename you want to use
to log output information. By default, Transporter logs output
information to standard out. If you specify a filename,
Transporter logs the output to the specified file, as well as to
standard out.
-v <arg> The -v option specifies the level of logging. The five values
are: off, detailed, informational, critical, eXtreme.
-version The -version option should be used by itself and returns the
version of the tool.
-WONoPause <arg> The -WONoPause option is only valid on Windows and its value can
be 'true' or 'false'. If an error occurs during script execution,
the process idles because the message 'Press any key...' is
displayed on the console and the system awaits a keypress. To
avoid this behavior, set this property to true
-Xmx4096m Specifies that you want to change the Java Virtual Machine's (JVM)
allocated memory by increasing the JVM heap size. By default,
Transporter uses a 2048MB heap size. You can use the -Xmx4096m
option to specify a 4-gigabyte (GB) heap size. Apple recommends,
if needed, increasing the heap size to 4096MB by specifying the
-Xmx4096m (or -Xmx4g) option and adjusting as needed.

Read more

How to send message from bot to Slack in Swift

Issue #560

Create a bot

Post message

After adding bot to workspace, we’ll get OAuth Access Token and Bot User OAuth Access Token. Use Bot User OAuth Access Token to test drive bot message sending

https://api.slack.com/methods/chat.postMessage/test

The request url is like

1
https://slack.com/api/chat.postMessage?token=xoxb-7212342835698-890815481123-abcdGgDEFfm2joQs1Vj5mABC&channel=random&text=hello&pretty=1

Code from Puma

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
import Foundation

public class Slack {
public var name: String = "Send message to Slack"
public var isEnabled = true

private var message: Message?

public init(_ closure: (Slack) -> Void = { _ in }) {
closure(self)
}
}

public extension Slack {
struct Message {
let token: String
let channel: String
let text: String
let username: String?

public init(
token: String,
channel: String,
text: String,
username: String
) {
self.token = token
self.channel = channel
self.text = text
self.username = username
}
}

func post(message: Message) {
self.message = message
}
}

extension Slack: Task {
public func run(workflow: Workflow, completion: @escaping TaskCompletion) {
guard let message = message else {
completion(.failure(PumaError.invalid))
return
}

let sender = MessageSender()
sender.send(message: message, completion: { result in
switch result {
case .success:
Deps.console.success("Message posted successfully")
case .failure(let error):
Deps.console.error("Failed: \(error.localizedDescription)")
}
completion(result)
})
}
}

private class MessageSender {
struct Response: Decodable {
let ok: Bool
let error: String?
}

func send(message: Slack.Message, completion: @escaping (Result<(), Error>) -> Void) {
guard let baseUrl = URL(string: "https://slack.com/api/chat.postMessage") else {
completion(.failure(PumaError.invalid))
return
}

var components = URLComponents(url: baseUrl, resolvingAgainstBaseURL: false)
components?.queryItems = [
URLQueryItem(name: "token", value: message.token),
URLQueryItem(name: "channel", value: message.channel),
URLQueryItem(name: "text", value: message.text),
URLQueryItem(name: "pretty", value: "1")
]

if let username = message.username {
components?.queryItems?.append(
URLQueryItem(name: "username", value: username)
)
}

guard let requestUrl = components?.url else {
completion(.failure(PumaError.invalid))
return
}

var request = URLRequest(url: requestUrl)
request.allHTTPHeaderFields = [
"Accept": "application/json"
]

let task = URLSession.shared.dataTask(with: request, completionHandler: { data, response, error in
guard let data = data else {
completion(.failure(error ?? PumaError.invalid))
return
}

do {
let response = try JSONDecoder().decode(Response.self, from: data)
if response.ok {
completion(.success(()))
} else {
completion(.failure(PumaError.from(string: response.error)))
}
} catch {
completion(.failure(error))
}
})

task.resume()
}
}

Read more

How to parse xcrun simctl devices

Issue #559

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
public class GetDestinations {
public init() {}

public func getAvailable(workflow: Workflow) throws -> [Destination] {
let processHandler = DefaultProcessHandler(filter: { $0.starts(with: "name=") })
let string = try CommandLine().runBash(
workflow: workflow,
program: "xcrun simctl",
arguments: [
"list",
"devices",
"-j"
],
processHandler: processHandler
)

guard let data = string.data(using: .utf8) else {
throw PumaError.invalid
}

let response: Response = try JSONDecoder().decode(Response.self, from: data)
let devicesWithOS: [DeviceWithOS] = response.devices.flatMap({ key, value in
return value.map({ DeviceWithOS(device: $0, os: key) })
})

let destinations: [Destination] = try devicesWithOS
.filter({ withOS in
return withOS.device.isAvailable
})
.compactMap({ withOS in
guard
let platform = self.platform(withOS: withOS),
let os = try self.os(withOS: withOS)
else {
return nil
}

var destination = Destination(
name: withOS.device.name,
platform: platform,
os: os
)
destination.id = withOS.device.udid
return destination
})

return destinations
}

func findId(workflow: Workflow, destination: Destination) throws -> String? {
let availableDestinations = try self.getAvailable(workflow: workflow)
return availableDestinations.first(where: { $0 == destination })?.id
}

// MARK: - Private

private func platform(withOS: DeviceWithOS) -> String? {
let list: [String] = [
Destination.Platform.iOS,
Destination.Platform.watchOS,
Destination.Platform.macOS,
Destination.Platform.tvOS,
]

return list.first(where: { withOS.os.contains($0) })
}

// com.apple.CoreSimulator.SimRuntime.iOS-13-2
private func os(withOS: DeviceWithOS) throws -> String? {
guard let string = try withOS.os.matches(pattern: #"(-\d+)+"#).first else {
return nil
}

return string.dropFirst().replacingOccurrences(of: "-", with: ".")
}
}

private struct Response: Decodable {
let devices: [String: [Device]]
}

private struct Device: Decodable {
let state: String
let name: String
let udid: String
let isAvailable: Bool
}

private struct DeviceWithOS {
let device: Device
let os: String
}

How to parse xcrun instruments devices

Issue #558

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
public class GetDestinations {
public init() {}

public func getAvailable(workflow: Workflow) throws -> [Destination] {
let processHandler = DefaultProcessHandler(filter: { $0.starts(with: "name=") })
let string = try CommandLine().runBash(
workflow: workflow,
program: "xcrun instruments",
arguments: [
"-s",
"devices"
],
processHandler: processHandler
)

// Ex: iPad Air (11.0.1) [7A5EAD29-D870-49FB-9A9B-C81079620AC9] (Simulator)
let destinations: [Destination] = try string
.split(separator: "\n")
.map({ String($0) })
.filter({ try $0.hasPattern(pattern: #"\[.+\]"#) })
.compactMap({ (line) -> Destination? in
parse(line)
})

return destinations
}

func parse(_ line: String) -> Destination? {
guard var id = try? line.matches(pattern: #"\[.+\]"#).first else {
return nil
}

var line = line
line = line.replacingOccurrences(of: id, with: "")
id = id
.replacingOccurrences(of: "[", with: "")
.replacingOccurrences(of: "]", with: "")

let isSimulator = line.contains("(Simulator)")
line = line.replacingOccurrences(of: "(Simulator)", with: "")

var os = (try? line.matches(pattern: #"\((\d+\.)?(\d+\.)?(\*|\d+)\)"#).first) ?? ""
let name = line
.replacingOccurrences(of: os, with: "")
.trimmingCharacters(in: .whitespacesAndNewlines)

os = os.replacingOccurrences(of: "(", with: "")
.replacingOccurrences(of: ")", with: "")

let device = self.device(name: name)

if os.isEmpty {
return Destination(name: name, id: id)
} else {
let platform = isSimulator ? "\(device) Simulator" : device
return Destination(name: name, platform: platform, os: os)
}
}

// MARK: - Private

private func device(name: String) -> String {
if name.starts(with: "iPad") || name.starts(with: "iPhone") {
return Destination.Platform.iOS
} else if name.starts(with: "Apple Watch") {
return Destination.Platform.watchOS
} else if name.starts(with: "Apple TV") {
return Destination.Platform.tvOS
} else if name.containsIgnoringCase("mac") {
return Destination.Platform.macOS
} else {
return Destination.Platform.iOS
}
}
}

How to generate xml in Swift

Issue #556

Instead of learning XMLParser, we can make a lightweight version

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

public protocol XmlItem {
func toLines() -> [String]
}

public struct XmlString: XmlItem {
public let key: String
public let value: String

public init(key: String, value: String) {
self.key = key
self.value = value
}

public func toLines() -> [String] {
return [
"<key>\(key)</key>",
"<string>\(value)</string>"
] as [String]
}
}

public struct XmlBool: XmlItem {
public let key: String
public let value: Bool

public init(key: String, value: Bool) {
self.key = key
self.value = value
}

public func toLines() -> [String] {
let string = value ? "<true/>" : "<false/>"
return [
"<key>\(key)</key>",
"\(string)"
] as [String]
}
}

public struct XmlDict: XmlItem {
public let key: String
public let items: [XmlItem]

public init(key: String, items: [XmlItem]) {
self.key = key
self.items = items
}

public func toLines() -> [String] {
var lines = [String]()
lines.append("<dict>")
lines.append(contentsOf: items.flatMap({ $0.toLines() }))
lines.append("</dict>")

return lines
}
}

public class XmlGenerator {
public init() {}
public func generateXml(_ items: [XmlItem]) -> String {
let content = items.flatMap({ $0.toLines() }).joined(separator: "\n")
let xml =
"""
<?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>
\(content)
</dict>
</plist>
"""
return xml
}

public func xmlItems(dictionary: [String: Any]) -> [XmlItem] {
return dictionary.flatMap({ (key, value) -> [XmlItem] in
switch value {
case let string as String:
return [XmlString(key: key, value: string)]
case let bool as Bool:
return [XmlBool(key: key, value: bool)]
case let nestedDictionary as [String: Any]:
return xmlItems(dictionary: nestedDictionary)
default:
return []
}
})
}
}

How to use synthetic property in Kotlin Android Extension

Issue #555

Synthetic properties generated by Kotlin Android Extensions plugin needs a view for Fragment/Activity to be set before hand.

In your case, for Fragment, you need to use view.btn_K in onViewCreated

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    super.onCreateView(inflater, container, savedInstanceState)
    val view = inflater.inflate(R.layout.fragment_card_selector, container, false)
    view.btn_K.setOnClickListener{} // access with `view`
    return view
}

Or better, you should only access synthetic properties in onViewCreated

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    super.onCreateView(inflater, container, savedInstanceState)
    return inflater.inflate(R.layout.fragment_card_selector, container, false)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    btn_K.setOnClickListener{} // access without `view`
}

Please notice that savedInstanceState parameter should be nullable Bundle?, and also check Importing synthetic properties

It is convenient to import all widget properties for a specific layout
in one go:

import kotlinx.android.synthetic.main.<layout>.*

Thus if the layout filename is activity_main.xml, we’d import
kotlinx.android.synthetic.main.activity_main.*.

If we want to call the synthetic properties on View, we should also
import kotlinx.android.synthetic.main.activity_main.view.*.

Read more

How to use KVO in Swift

Issue #554

A class must inherit from NSObject, and we have 3 ways to trigger property change

Use setValue(value: AnyObject?, forKey key: String) from NSKeyValueCoding

class MyObjectToObserve: NSObject {
    var myDate = NSDate()
    func updateDate() {
        setValue(NSDate(), forKey: "myDate")
    }
}

Use willChangeValueForKey and didChangeValueForKey from NSKeyValueObserving

class MyObjectToObserve: NSObject {
    var myDate = NSDate()
    func updateDate() {
        willChangeValueForKey("myDate")
        myDate = NSDate()
        didChangeValueForKey("myDate")
    }
}

Use dynamic. See Swift Type Compatibility

You can also use the dynamic modifier to require that access to members be dynamically dispatched through the Objective-C runtime if you’re using APIs like key–value observing that dynamically replace the implementation of a method.

class MyObjectToObserve: NSObject {
    dynamic var myDate = NSDate()
    func updateDate() {
        myDate = NSDate()
    }
}

And property getter and setter is called when used. You can verify when working with KVO. This is an example of computed property

class MyObjectToObserve: NSObject {
    var backing: NSDate = NSDate()
    dynamic var myDate: NSDate {
        set {
            print("setter is called")
            backing = newValue
        }
        get {
            print("getter is called")
            return backing
        }
    }
}

Read more

How to use precondition and assert in Swift

Issue #553

Read Swift asserts - the missing manual

1
2
3
4
5
6
7
                        debug	release	  release
function -Onone -O -Ounchecked
assert() YES NO NO
assertionFailure() YES NO NO**
precondition() YES YES NO
preconditionFailure() YES YES YES**
fatalError()* YES YES YES

And from Interesting discussions on Swift Evolution

– assert: checking your own code for internal errors

– precondition: for checking that your clients have given you valid arguments.

Read more

How to get ISO string from date in Javascript

Issue #552

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
type Components = {
day: number,
month: number,
year: number
}

export default class DateFormatter {
// 2018-11-11T00:00:00
static ISOStringWithoutTimeZone = (date: Date): string => {
const components = DateFormatter.format(DateFormatter.components(date))
return `${components.year}-${components.month}-${components.day}T00:00:00`
}

static format = (components: Components) => {
return {
day: `${components.day}`.padStart(2, '0'),
month: `${components.month}`.padStart(2, '0'),
year: components.year
}
}

static components = (date: Date): Components => {
return {
day: date.getDate(),
month: date.getMonth() + 1,
year: date.getFullYear()
}
}
}

Read more

How to use regular expression in Swift

Issue #551

Find matches

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 Foundation

public extension String {
func matches(pattern: String) throws -> [String] {
let regex = try NSRegularExpression(pattern: pattern)
let results = regex.matches(in: self, options: [], range: NSRange(self.startIndex..., in: self))
return results.compactMap({ result in
if let range = Range(result.range, in: self) {
return String(self[range])
} else {
return nil
}
})
}

func hasPattern(pattern: String) throws -> Bool {
return try !matches(pattern: pattern).isEmpty
}
}

func testRegex() throws {
let string = "iPad Air (11.0.1) [7A5EAD29-D870-49FB-9A9B-C81079620AC9] (Simulator)"
let matches = try string.matches(pattern: #"\[.+\]"#)
XCTAssertEqual(matches.first, "[7A5EAD29-D870-49FB-9A9B-C81079620AC9]")
}

Range and NSRange

1
2
let string: String
let nsRange: NSRange = NSRange(string.startIndex..., in: string)
1
2
3
4
let string: String
let nsRange: NSRange
let range: Range = Range(nsRange, in: string)
let substring: Substring = string[range]

Common expression

Version

1
2
3
#"\((\d+\.)?(\d+\.)?(\*|\d+)\)"#

(11.0.1)

Read more

© 2020 onmyway133 All Rights Reserved.
Theme by hiero