How to fix UIToolbar Auto Layout issues

Issue #538

Hierarchy

1
UIToolbar -> _UIToolbarContentView -> _UIButtonBarStackVie

UIToolbarContentView

1
2
_UIToolbarContentView's width should equal 0
_UIToolbarContentView's height should equal 0

Workaround that fixes 1 warning

1
2
toolbar.setItems(items, animated: false)
toolbar.updateConstraintsIfNeeded()

###

Set frame explicitly

Use a non .zero frame that is close to the view bounds width

1
2
3
4
let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: 375, height: 30))
DispatchQueue.main.async {
self.toolbar.updateConstraintsIfNeeded()
}

How to use passed launch arguments in UITests

Issue #537

Specify launch arguments

In xcodebuild, specify launch arguments.

You can specify this under Launch Arguments in Run action of the app scheme or UITest scheme

Screenshot 2019-12-10 at 23 27 02
1
-AppleLanguages (jp) -AppleLocale (jp_JP)
1
2
3
4
5
6
7
8
9
10
11
12
13
(lldb) po ProcessInfo().arguments
11 elements
- 0 : "/Users/khoa/Library/Developer/CoreSimulator/Devices/561F2B45-26B2-4897-98C4-8A917AEB48D2/data/Containers/Bundle/Application/436E0A43-8323-4F53-BBE0-6F75F674916F/TestAppUITests-Runner.app/TestAppUITests-Runner"
- 1 : "-AppleLanguages"
- 2 : "(ja)"
- 3 : "-AppleTextDirection"
- 4 : "NO"
- 5 : "-AppleLocale"
- 6 : "ja_JP"
- 7 : "-NSTreatUnknownArgumentsAsOpen"
- 8 : "NO"
- 9 : "-ApplePersistenceIgnoreState"
- 10 : "YES"

In UITests, pass launch arguments from UITest scheme to UITest application

1
app.launchArguments += ProcessInfo().arguments

Environments

1
ProcessInfo().environment // [String: String]

testLanguage and testRegion

No need for ProcessInfo().arguments

1
2
3
man xcodebuild

xcodebuild -project 'TestApp.xcodeproj' -scheme 'TestAppUITests' -configuration Debug -sdk iphonesimulator -UseModernBuildSystem=YES -destination 'OS=13.2.2,name=iPhone 11,platform=iOS Simulator' -testLanguage ja -testRegion ja_JP test

How to add padding to left right view in UITextField

Issue #536

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
extension UITextField {
func setLeftView(_ view: UIView, padding: CGFloat) {
view.translatesAutoresizingMaskIntoConstraints = true

let outerView = UIView()
outerView.translatesAutoresizingMaskIntoConstraints = false
outerView.addSubview(view)

outerView.frame = CGRect(
origin: .zero,
size: CGSize(
width: view.frame.size.width + padding,
height: view.frame.size.height + padding
)
)

view.center = CGPoint(
x: outerView.bounds.size.width / 2,
y: outerView.bounds.size.height / 2
)

leftView = outerView
}
}

How to work with git

Issue #531

Expand commits in Sublime Merge

1
2
3
4
{
"expand_merge_commits_by_default": true,
"translate_tabs_to_spaces": true
}

local hooks .git/hooks vs hooksPath

1
git config core.hooksPath ~/.git-templates/hooks

Only hooksPath gets run. Removing hooksPath make local hooks work

https://stackoverflow.com/questions/39332407/git-hooks-applying-git-config-core-hookspath

Use git templates

https://coderwall.com/p/jp7d5q/create-a-global-git-commit-hook

Define alias in zshrc

1
2
3
vim ~/.zshrc
alias check="~/.git-templates/hooks/check.sh"
source ~/.zshrc

pre-commit

https://itnext.io/using-git-hooks-to-enforce-branch-naming-policy-ffd81fa01e5e

1
(feature|fix|refactor)\/[a-z0-9-]+$

How to use decoration view in UICollectionView

Issue #529

indexPath

https://developer.apple.com/documentation/uikit/uicollectionviewlayoutattributes/1617786-layoutattributesfordecorationvie

It is up to you to decide how to use the indexPath parameter to identify a given decoration view. Typically, you use the decorationViewKind parameter to identify the type of the decoration view and the indexPath information to distinguish between different instances of that view.

How to do lense in Swift

Issue #528

What is lense

https://www.schoolofhaskell.com/school/to-infinity-and-beyond/pick-of-the-week/a-little-lens-starter-tutorial

A lens is a first-class reference to a subpart of some data type. For instance, we have _1 which is the lens that “focuses on” the first element of a pair. Given a lens there are essentially three things you might want to do

View the subpart
Modify the whole by changing the subpart
Combine this lens with another lens to look even deeper

Before, use functional approach

http://chris.eidhof.nl/post/lenses-in-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
struct Person {
let name_ : String
let address_ : Address
}

struct Address {
let street_ : String
let city_ : String
}

struct Lens<A,B> {
let from : A -> B
let to : (B, A) -> A
}

let address : Lens<Person,Address> = Lens(from: { $0.address_ }, to: {
Person(name_: $1.name_, address_: $0)
})

let street : Lens<Address,String> = Lens(from: { $0.street_ }, to: {
Address(street_: $0, city_: $1.city_)
})

let newAddress = street.to("My new street name", existingAddress)

Now, with Keypath

https://iankeen.tech/2018/06/05/type-safe-temporary-models/
https://swiftbysundell.com/articles/defining-testing-data-in-swift/

Use KeyPath to modify struct data

1
2
3
4
5
6
7
8
9
10
11
12
protocol Stubbable: Identifiable {
static func stub(withID id: Identifier<Self>) -> Self
}

extension Stubbable {
func setting<T>(_ keyPath: WritableKeyPath<Self, T>,
to value: T) -> Self {
var stub = self
stub[keyPath: keyPath] = value
return stub
}
}

How to convert from callback to Future Publisher in Combine

Issue #527

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

public typealias TaskCompletion = (Result<(), Error>) -> Void

public protocol Task: AnyObject {
var name: String { get }
func run(workflow: Workflow, completion: TaskCompletion)
}

public extension Task {
func asPublisher(workflow: Workflow) -> AnyPublisher<(), Error> {
return Future({ completion in
self.run(workflow: workflow, completion: completion)
}).eraseToAnyPublisher()
}
}

let sequence = Publishers.Sequence<[AnyPublisher<(), Error>], Error>(
sequence: tasks.map({ $0.asPublisher(workflow: self) })
)

How to make init with closure in Swift

Issue #526

1
2
3
4
5
6
7
public class Build: UsesXcodeBuild {
public var arguments = [String]()

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

Use function builder

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
public class Workflow {
public var workingDirectory: String = "."
public let tasks: [Task]

public init(@TaskBuilder builder: () -> [Task]) {
self.tasks = builder()
self.tasks.forEach { task in
task.workflow = self
}
}

public init(@TaskBuilder builder: () -> Task) {
self.tasks = [builder()]
self.tasks.forEach { task in
task.workflow = self
}
}
}

public func run(@TaskBuilder builder: () -> [Task]) {
let workflow = Workflow(builder: builder)
workflow.run()
}

public func run(@TaskBuilder builder: () -> Task) {
let workflow = Workflow(builder: builder)
workflow.run()
}

How to test a developing package with Swift Package Manager

Issue #525

Use macOS Command Line project

Example Puma

  • Create a new macOS project, select Command Line ToolScreenshot 2019-11-30 at 22 40 35
  • Drag Puma.xcodeproj as a sub project of our test project
  • Go to our TestPuma target, under Link Binary with Libraries, select Puma framework
Screenshot 2019-11-30 at 22 41 18
  • Puma has dependencies on PumaCore and PumaiOS, but in Xcode we only need to select Puma framework

  • In code, we need to explicitly import PumaiOS framework if we use any of its classes

1
2
3
4
5
6
7
8
9
10
11
import Foundation
import Puma
import PumaiOS

func testDrive() {
run {
SetVersionNumber {
$0.buildNumberForAllTarget("1.1")
}
}
}
  • As our Puma.xcodeproj is inside this test project, we can drill down into our Puma.xcodeproj and update the code.

Troubleshooting

Code signing for frameworks

To avoid signing issue, we need to select a Team for all frameworks

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?

Library not loaded

Need to set runpath search path, read https://stackoverflow.com/questions/28577692/macos-command-line-tool-with-swift-cocoa-framework-library-not-loaded

Specify LD_RUNPATH_SEARCH_PATHS = @executable_path in Build Settings

How to use method from protocol extension in Swift

Issue #524

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/// Any task that uses command line
public protocol UsesCommandLine: AnyObject {
var program: String { get }
var arguments: Set<String> { get set }
}

public extension UsesCommandLine {
func run() throws {
let command = "\(program) \(arguments.joined(separator: " "))"
Log.command(command)
_ = try Process().run(command: command)
}
}

class Build: UsesCommandLine {
public func run() throws {
arguments.insert("build")
try (self as UsesCommandLine).run()
}
}

How to organize dependencies in Swift Package Manager

Issue #523

In Puma I want to make build tools for iOS and Android, which should share some common infrastructure. So we can organize dependencies like.

Puma -> PumaAndroid, PumaiOS -> PumaCore -> xcbeautify, Files, Colorizer

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
// 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: "Puma",
platforms: [.macOS("10.15")],
products: [
.library(name: "Puma", targets: ["Puma"])
],
dependencies: [
.package(
url: "https://github.com/thii/xcbeautify.git",
.upToNextMajor(from: "0.4.1")
),
.package(
url: "https://github.com/getGuaka/Colorizer",
.upToNextMajor(from: "0.2.0")
),
.package(
url: "https://github.com/JohnSundell/Files.git",
.upToNextMajor(from : "3.1.0")
)
],
targets: [
.target(
name: "Puma",
dependencies: [
"PumaiOS",
"PumaAndroid",
],
path: "Sources/Puma"
),
.target(
name: "PumaCore",
dependencies: [
"XcbeautifyLib",
"Colorizer",
"Files"
],
path: "Sources/Core"
),
.target(
name: "PumaiOS",
dependencies: [
"PumaCore"
],
path: "Sources/iOS"
),
.target(
name: "PumaAndroid",
dependencies: [
"PumaCore"
],
path: "Sources/Android"
),
.testTarget(
name: "PumaTests",
dependencies: ["Puma"
]
)
]
)

How to provide configurations in Swift

Issue #522

Sometimes ago I created Puma, which is a thin wrapper around Xcode commandline tools, for example xcodebuild

There’s lots of arguments to pass in xcodebuild, and there are many tasks like build, test and archive that all uses this command.

Use Options struct to encapsulate parameters

To avoid passing many parameters into a class, I tend to make an Options struct to encapsulate all passing parameters. I also use composition, where Build.Options and Test.Options contains Xcodebuild.Options

This ensures that the caller must provide all needed parameters, when you can compile you are ensured that all required parameters are provided.

This is OK, but a bit rigid in a way that there are many more parameters we can pass into xcodebuild command, so we must provide a way for user to alter or add more parameters.

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
let xcodebuildOptions = Xcodebuild.Options(
workspace: nil,
project: "TestApp",
scheme: "TestApp",
configuration: Configuration.release,
sdk: Sdk.iPhone,
signing: .auto(automaticSigning),
usesModernBuildSystem: true
)

run {
SetVersionNumber(options: .init(buildNumber: "1.1"))
SetBuildNumber(options: .init(buildNumber: "2"))
Build(options: .init(
buildOptions: xcodebuildOptions,
buildsForTesting: true
))

Test(options: .init(
buildOptions: xcodebuildOptions,
destination: Destination(
platform: Destination.Platform.iOSSimulator,
name: Destination.Name.iPhoneXr,
os: Destination.OS.os12_2
)
))
}

Here is how to convert from Options to arguments to pass to our command. Because each parameter has different specifiers, like with double hyphens --flag=true, single hyphen -flag=true or just hyphen with a space between parameter key and value -flag true, we need to manually specify that, and concat them with string. Luckily, the order of parameters is not important

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
public struct Xcodebuild {
public struct Options {
/// build the workspace NAME
public let workspace: String?
/// build the project NAME
public let project: String
/// build the scheme NAME
public let scheme: String
/// use the build configuration NAME for building each target
public let configuration: String
/// use SDK as the name or path of the base SDK when building the project
public let sdk: String?
public let signing: Signing?
public let usesModernBuildSystem: Bool

public init(
workspace: String? = nil,
project: String,
scheme: String,
configuration: String = Configuration.debug,
sdk: String? = Sdk.iPhoneSimulator,
signing: Signing? = nil,
usesModernBuildSystem: Bool = true) {

self.workspace = workspace
self.project = project
self.scheme = scheme
self.configuration = configuration
self.sdk = sdk
self.signing = signing
self.usesModernBuildSystem = usesModernBuildSystem
}
}
}

extension Xcodebuild.Options {
func toArguments() -> [String?] {
return [
workspace.map{ "-workspace \($0.addingFileExtension("xcworkspace"))" },
"-project \(project.addingFileExtension("xcodeproj"))",
"-scheme \(scheme)",
"-configuration \(configuration)",
sdk.map { "-sdk \($0)" },
"-UseModernBuildSystem=\(usesModernBuildSystem ? "YES": "NO")"

]
}
}

Use convenient methods

Another way is to have a Set<String> as a container of parameters, and provide common method via protocol extension

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
/// Any task that uses command line
public protocol UsesCommandLine: AnyObject {
var program: String { get }
var arguments: Set<String> { get set }
}

public extension UsesCommandLine {
func run() throws {
let command = "\(program) \(arguments.joined(separator: " "))"
Log.command(command)
_ = try Process().run(command: command)
}
}

/// Any task that uses xcodebuild
public protocol UsesXcodeBuild: UsesCommandLine {}

public extension UsesXcodeBuild {
var program: String { "xcodebuild" }

func `default`(project: String, scheme: String) {
self.project(project)
self.scheme(scheme)
self.configuration(Configuration.debug)
self.sdk(Sdk.iPhoneSimulator)
self.usesModernBuildSystem(enabled: true)
}

func project(_ name: String) {
arguments.insert("-project \(name.addingFileExtension("xcodeproj"))")
}

func workspace(_ name: String) {
arguments.insert("-workspace \(name.addingFileExtension("xcworkspace"))")
}

func scheme(_ name: String) {
arguments.insert("-scheme \(name)")
}

func configuration(_ configuration: String) {
arguments.insert("-configuration \(configuration)")
}

func sdk(_ sdk: String) {
arguments.insert("-sdk \(sdk)")
}

func usesModernBuildSystem(enabled: Bool) {
arguments.insert("-UseModernBuildSystem=\(enabled ? "YES": "NO")")
}
}

class Build: Task, UsesXcodeBuild {}
class Test: Task, UsesXcodeBuild {}

Now the call site looks like this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
run {
SetVersionNumber {
$0.versionNumberForAllTargets("1.1")
}

SetBuildNumber {
$0.buildNumberForAllTargets("2")
}

Build {
$0.default(project: "TestApp", scheme: "TestApp")
$0.buildsForTesting(enabled: true)
}

Test {
$0.default(project: "TestApp", scheme: "TestApp")
$0.testsWithoutBuilding(enabled: true)
$0.destination(Destination(
platform: Destination.Platform.iOSSimulator,
name: Destination.Name.iPhoneXr,
os: Destination.OS.os12_2
))
}
}

How to use SurveyMonkey in React Native

Issue #521

1
2
3
4
5
#import <React/RCTBridgeModule.h>

@interface RNSurveyManager : NSObject <RCTBridgeModule>

@end
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 "RNSurveyManager.h"
#import <React/RCTLog.h>
#import <SurveyMonkeyiOSSDK/SurveyMonkeyiOSSDK.h>

@interface RNSurveyManager()<SMFeedbackDelegate>
@property (nonatomic, strong) SMFeedbackViewController * feedbackController;
@end

@implementation RNSurveyManager

- (instancetype)init {
self = [super init];
if (self) {
self.feedbackController = [[SMFeedbackViewController alloc] initWithSurvey:@"VV8X5QA"];
self.feedbackController.delegate = self;
}
return self;
}

+ (BOOL)requiresMainQueueSetup {
return YES;
}

- (dispatch_queue_t)methodQueue {
return dispatch_get_main_queue();
}

RCT_EXPORT_MODULE(SurveyManager);

- (void)respondentDidEndSurvey:(SMRespondent *)respondent error:(NSError *)error {
NSLog(@"respondent %@ error %@", respondent, error);
}

RCT_EXPORT_METHOD(show:(RCTResponseSenderBlock)callback) {
[UIApplication.sharedApplication.keyWindow.rootViewController
presentViewController:self.feedbackController
animated:YES
completion:^() {
callback(@[]);
}];
}

@end
1
2
3
4
5
6
7
8
9
10
// @flow

import { NativeModules } from 'react-native'

type SurveyManagerType = {
show: () => void
}

const SurveyManager: SurveyManagerType = NativeModules.SurveyManager
export default SurveyManager

How to test UserDefaults in iOS

Issue #518

1
2
let userDefaults = UserDefaults(suiteName: suiteName)
userDefaults.removePersistentDomain(forName: suiteName)

https://developer.apple.com/documentation/foundation/userdefaults/1417339-removepersistentdomain

Calling this method is equivalent to initializing a user defaults object with init(suiteName:) passing domainName, and calling the removeObject(forKey:) method on each of its keys.

Read more

How to use ForEach with ScrollView in SwiftUI

Issue #517

Use ScrollView -> VStack -> ForEach -> Content

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct SearchScreen: View {
@State var searchObjects: [SearchObject] = [
SearchObject(name: "By name", search: { CountryManager.shared.search(byName: $0) }),
SearchObject(name: "By calling code", search: { CountryManager.shared.search(byCallingCode: $0) }),
SearchObject(name: "By domain", search: { CountryManager.shared.search(byDomain: $0) }),
SearchObject(name: "By language", search: { CountryManager.shared.search(byLanguage: $0) })
]

var body: some View {
ScrollView {
VStack(alignment: .leading) {
ForEach(searchObjects.enumerated().map({ $0 }), id: \.element.name, content: { index, searchObject in
VStack(alignment: .leading) {
Text(searchObject.name)
.styleLabel()
TextField(searchObject.textFieldName, text: self.$searchObjects[index].text)
.styleTitle()
self.makeButton(searchObject: self.searchObjects[index])
}
})
}
}
}
}

How to modify data inside array in SwiftUI

Issue #516

Suppose we have an array of SearchObject, and user can enter search query into text property.

1
2
3
4
5
6
7
8
9
10
class SearchObject: ObservableObject {
let name: String
let search: (String) -> [Country]
var text: String = ""

init(name: String, search: @escaping (String) -> [Country]) {
self.name = name
self.search = search
}
}

Although SearchObject is class, when we use ForEach, the changes to passed object won’t be reflected in our array and there is no reload trigger, we need to point to object in array directly, like

1
self.$searchObjects[index].text
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct SearchScreen: View {
@State var searchObjects: [SearchObject] = [
SearchObject(name: "By name", search: { CountryManager.shared.search(byName: $0) }),
SearchObject(name: "By calling code", search: { CountryManager.shared.search(byCallingCode: $0) }),
SearchObject(name: "By domain", search: { CountryManager.shared.search(byDomain: $0) }),
SearchObject(name: "By language", search: { CountryManager.shared.search(byLanguage: $0) })
]

var body: some View {
ScrollView {
VStack(alignment: .leading) {
ForEach(searchObjects.enumerated().map({ $0 }), id: \.element.name, content: { index, searchObject in
VStack(alignment: .leading) {
Text(searchObject.name)
.styleLabel()
TextField(searchObject.textFieldName, text: self.$searchObjects[index].text)
.styleTitle()
self.makeButton(searchObject: self.searchObjects[index])
}
})
}
}
}
}

How to use index in SwiftUI list

Issue #515

Use enumerated and id: \.element.name

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct CountriesView: View {
let countries: [Country]

var body: some View {
let withIndex = countries.enumerated().map({ $0 })

return List(withIndex, id: \.element.name) { index, country in
NavigationLink(
destination: CountryView(country: country),
label: {
VStack(alignment: .leading) {
Text(country.name)
.styleMultiline()
}
.paddingVertically()
}
)
}
}
}

How to setup multiple git accounts for GitHub and Bitbucket

Issue #514

Generate SSH keys

1
2
3
4
5
6
7
8
9
ssh-keygen -t rsa -C "onmyway133@gmail.com" -f "id_rsa_github"
ssh-keygen -t rsa -C "onmyway133bitbucket@gmail.com" -f "id_rsa_bitbucket"

pbcopy < ~/.ssh/id_rsa_github.pub
pbcopy < ~/.ssh/id_rsa_bitbucket.pub

ssh-add -D
ssh-add id_rsa_github
ssh-add id_rsa_bitbucket
1
2
3
4
5
6
7
8
9
10
11
12
13
vim ~/.ssh/config

#Github (personal)
Host gh
HostName github.com
User git
IdentityFile ~/.ssh/id_rsa_github

#Bitbucket (work)
Host bb
HostName bitbucket.org
User git
IdentityFile ~/.ssh/id_rsa_bitbucket

Config

1
2
git config --global user.email "onmyway133@gmail.com"
git config --local user.email "onmyway133bitbucket@gmail.com"

Read more

How to use objectWillChange in Combine

Issue #513

A publisher that emits before the object has changed

Use workaround DispatchQueue to wait another run loop to access newValue

1
2
3
4
5
6
7
8
9
.onReceive(store.objectWillChange, perform: {
DispatchQueue.main.async {
self.reload()
}
})

func reload() {
self.isFavorite = store.isFavorite(country: country)
}

Read more

How to show list with section in SwiftUI

Issue #511

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 CountriesView: View {
let groups: [Group]

init(countries: [Country]) {
self.groups = CountryManager.shared.groups(countries: countries)
}

var body: some View {
List {
ForEach(groups) { group in
Section(
header:
Text(group.initial)
.foregroundColor(Color.yellow)
.styleTitle(),
content: {
ForEach(group.countries) { country in
CountryRow(country: country)
}
}
)
}
}
}
}

How to group array by property in Swift

Issue #510

Use Dictionary(grouping:by:)

1
2
3
4
5
6
7
8
9
10
func groups(countries: [Country]) -> [Group] {
let dictionary = Dictionary(grouping: countries, by: { String($0.name.prefix(1)) })
let groups = dictionary
.map({ (key: String, value: [Country]) -> Group in
return Group(initial: key, countries: value)
})
.sorted(by: { $0.initial < $1.initial })

return groups
}

Links for tools

Issue #509

Xcode

Terminal

Apps

© 2019 onmyway133 All Rights Reserved.
Theme by hiero