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


Updated at 2020-10-04 06:52:07

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
}

How to make full width list row in SwiftUI

Issue #508

We need to use frame(minWidth: 0, maxWidth: .infinity, alignment: .leading). Note that order is important, and padding should be first, and background after frame to apply color to the entire frame

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
struct BooksScreen: View {
@ObservedObject var viewModel: BooksViewModel

var body: some View {
List {
ForEach(viewModel.books) { book in
RowView(vault: book)
}
}
.listStyle(GroupedListStyle())
}
}

private struct RowView: View {
let book: Book

var body: some View {
VStack(alignment: .leading) {
Text(book.name)
.foregroundColor(.white)
.font(.headline)
Text(book.text)
.foregroundColor(.white)
.font(.subheadline)
}
.padding()
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
.background(Color(hex: book.color))
.cornerRadius(8)
}
}

How to make full screen TabView in SwiftUI

Issue #507

View extends to the bottom, but not to the notch. We need to add .edgesIgnoringSafeArea(.top) to our TabView to tell TabView to extend all the way to the top.

Note that if we use edgesIgnoringSafeArea(.all) then TabView ‘s bar will be dragged very down and broken.

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
struct MainScreen: View {
init() {
UITabBar.appearance().backgroundColor = R.color.barBackground
UITableView.appearance().backgroundColor = .clear
UITableViewCell.appearance().backgroundColor = .clear
UITableView.appearance().tableFooterView = UIView()
}

var body: some View {
ZStack {
R.color.background
.edgesIgnoringSafeArea(.all)

TabView {
PersonalScreen()
.tabItem({
Image(sfSymbol: .bagFill)
Text("Personal")
.styleTabBarItem()
})
CloudScreen()
.tabItem({
Image(sfSymbol: .cloudFill)
Text("Cloud")
.styleTabBarItem()
})
SettingsScreen()
.tabItem({
Image(sfSymbol: .gear)
Text("Settings")
.styleTabBarItem()
})
}
.edgesIgnoringSafeArea(.top)
}
}
}

How to map error in Combine

Issue #506

When a function expects AnyPublisher<[Book], Error> but in mock, we have Just

1
2
3
4
5
6
7
func getBooks() -> AnyPublisher<[Book], Error> {
return Just([
Book(id: "1", name: "Book 1"),
Book(id: "2", name: "Book 2"),
])
.eraseToAnyPublisher()
}

There will be a mismatch, hence compile error

Cannot convert return expression of type ‘AnyPublisher<[Book], Just.Failure>’ (aka ‘AnyPublisher<Array, Never>’) to return type ‘AnyPublisher<[Book], Error>’

The reason is because Just produces Never, not Error. The workaround is to introduce Error

1
2
3
enum AppError: Error {
case impossible
}
1
2
3
4
5
6
7
8
func getBooks() -> AnyPublisher<[Book], Error> {
return Just([
Book(id: "1", name: "Book 1"),
Book(id: "2", name: "Book 2"),
])
.mapError({ _ in AppError.impossible })
.eraseToAnyPublisher()
}

Updated at 2020-11-07 20:30:01

How to make Swift Package Manager package for multiple platforms

Issue #504

https://twitter.com/NeoNacho/status/1181245484867801088?s=20

There’s no way to have platform specific sources or targets today, so you’ll have to take a different approach. I would recommend wrapping all OS specific files in #if os and just having one target. For tests, you could do something similar, one test target, but conditional tests

Every files are in Sources folder, so we can use platform and version checks. For example Omnia is a Swift Package Manager that supports iOS, tvOS, watchOS, macOS and Catalyst.

For macOS only code, need to check for AppKit and Catalyst

https://github.com/onmyway133/Omnia/blob/master/Sources/macOS/ClickedCollectionView.swift

1
#if canImport(AppKit) && !targetEnvironment(macCatalyst)

For SwiftUI feature, need to check for iOS 13 and macOS 10.15

https://github.com/onmyway133/Omnia/blob/master/Sources/SwiftUI/Utils/ImageLoader.swift

1
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)

How to make simple Redux for SwiftUI

Issue #502

Mutation is used to mutate state synchronously. Action is like intent, either from app or from user action. Action maps to Mutation in form of Publisher to work with async action, similar to redux-observable

AnyReducer is a type erasure that takes the reduce function

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 Combine
import Foundation

public protocol Reducer {
associatedtype State
associatedtype Mutation
func reduce(state: State, mutation: Mutation) -> State
}

public struct AnyReducer<State, Mutation> {
public let reduce: (State, Mutation) -> State
public init<R: Reducer>(reducer: R) where R.State == State, R.Mutation == Mutation {
self.reduce = reducer.reduce
}
}

public protocol Action {
associatedtype Mutation
func toMutation() -> AnyPublisher<Mutation, Never>
}

public final class Store<State, Mutation>: ObservableObject {
@Published public private(set) var state: State
public let reducer: AnyReducer<State, Mutation>
public private(set) var cancellables = Set<AnyCancellable>()

public init(initialState: State, reducer: AnyReducer<State, Mutation>) {
self.state = initialState
self.reducer = reducer
}

public func send<A: Action>(action: A) where A.Mutation == Mutation {
action
.toMutation()
.receive(on: DispatchQueue.main)
.sink(receiveValue: update(mutation:))
.store(in: &cancellables)
}

public func update(mutation: Mutation) {
self.state = reducer.reduce(state, mutation)
}
}

To use, conform to all the protocols. Also make typelias AppStore in order to easy specify type in SwiftUI 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
27
28
29
30
31
import SwiftUI
import Combine

typealias AppStore = Store<AppState, AppMutation>

let appStore: AppStore = AppStore(
initialState: AppState(),
reducer: appReducer
)

struct AppState: Codable {
var hasShownOnboaring = false
}

struct AppReducer: Reducer {
func reduce(state: AppState, mutation: AppMutation) -> AppState {
var state = state
switch mutation {
case .finishOnboarding:
state.hasShownOnboaring = true
@unknown default:
break
}

return state
}
}

enum AppMutation {
case finishOnboarding
}

Use in SwiftUI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct RootScreen: View {
@EnvironmentObject var store: AppStore

var body: some View {
if store.state.hasShownOnboaring {
return Text("Welcome")
.eraseToAnyView()
} else {
return OnboardingScreen()
.eraseToAnyView()
}
}
}

struct OnboardingScreen: View {
@EnvironmentObject var store: AppStore

private func done() {
store.send(action: AppAction.finishOnboarding)
}
}

Reference

How to use Firebase in macOS

Issue #501

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

target 'MyApp' do
use_frameworks!

pod 'FirebaseCore'
pod 'Firebase/Firestore'

end

Troubleshooting

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

Screenshot 2019-11-12 at 14 53 03

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

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

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

How to use Xcode

Issue #499

Build setting

Build Library For Distribution

It turns on all the features that are necessary to build your library in such a way that it can be distributed

What does this error actually mean? Well, when the Swift compiler goes to import a module, it looks for a file called the Compiled Module for that library. If it finds one of these files, it reads off the manifest of public APIs that you can call into, and lets you use them. Now, this Compiled Module Format is a binary format that basically contains internal compiler data structures.

And since they’re just internal data structures, they’re subject to change with every version of the Swift Compiler. So what this means is that if one person tries to import a module using one version of Swift, and that module was created by another version of Swift, their compiler can’t understand it, and they won’t be able to use it.

Well, in order to solve this version lock, Xcode 11 introduces a new format for Swift Modules, called Swift Module Interfaces. And just like the Compiled Module Format, they list out all the public APIs of a module, but in a textual form that behaves more like source code. And since they behave like source code, then future versions of the Swift Compiler will be able to import module interfaces created with older versions. And when you enable Build Libraries for Distribution, you’re telling the compiler to generate one of these stable interfaces whenever it builds your framework

Links for Xcode

Issue #499

Build setting

Build Library For Distribution

It turns on all the features that are necessary to build your library in such a way that it can be distributed

What does this error actually mean? Well, when the Swift compiler goes to import a module, it looks for a file called the Compiled Module for that library. If it finds one of these files, it reads off the manifest of public APIs that you can call into, and lets you use them. Now, this Compiled Module Format is a binary format that basically contains internal compiler data structures.

And since they’re just internal data structures, they’re subject to change with every version of the Swift Compiler. So what this means is that if one person tries to import a module using one version of Swift, and that module was created by another version of Swift, their compiler can’t understand it, and they won’t be able to use it.

Well, in order to solve this version lock, Xcode 11 introduces a new format for Swift Modules, called Swift Module Interfaces. And just like the Compiled Module Format, they list out all the public APIs of a module, but in a textual form that behaves more like source code. And since they behave like source code, then future versions of the Swift Compiler will be able to import module interfaces created with older versions. And when you enable Build Libraries for Distribution, you’re telling the compiler to generate one of these stable interfaces whenever it builds your framework

How to access view in fragment in Kotlin

Issue #497

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.*.


Original answer https://stackoverflow.com/questions/34541650/nullpointerexception-when-trying-to-access-views-in-a-kotlin-fragment/51674381#51674381

How to refresh receipt and restore in app purchase in iOS

Issue #496

Read this Restoring Purchased Products to understand the purposes between the 2.

From iOS 7, every app downloaded from the store has a receipt (for downloading/buying the app) at appStoreReceiptURL. When users purchases something via In App Purchase, the content at appStoreReceiptURL is updated with purchases information. Most of the cases, you just need to refresh the receipt (at appStoreReceiptURL) so that you know which transactions users have made.

Users restore transactions to maintain access to content they’ve already purchased. For example, when they upgrade to a new phone, they don’t lose all of the items they purchased on the old phone. Include some mechanism in your app to let the user restore their purchases, such as a Restore Purchases button. Restoring purchases prompts for the user’s App Store credentials, which interrupts the flow of your app: because of this, don’t automatically restore purchases, especially not every time your app is launched.

In most cases, all your app needs to do is refresh its receipt and deliver the products in its receipt. The refreshed receipt contains a record of the user’s purchases in this app, on this device or any other device. However, some apps need to take an alternate approach for one of the following reasons:

If you use Apple-hosted content, restoring completed transactions gives your app the transaction objects it uses to download the content.
If you need to support versions of iOS earlier than iOS 7, where the app receipt isn’t available, restore completed transactions instead.

Refreshing the receipt asks the App Store for the latest copy of the receipt. Refreshing a receipt does not create any new transactions.

Restoring completed transactions creates a new transaction for every completed transaction the user made, essentially replaying history for your transaction queue observer.

More about receipt, from WWDC 2017, What’s new in StoreKit session https://developer.apple.com/videos/play/wwdc2017/303/

enter image description here

You can also watch WWDC 2017, session Advanced StoreKit for more detail https://developer.apple.com/videos/play/wwdc2017/305/

enter image description here


Original answer https://stackoverflow.com/questions/45615106/when-to-refresh-a-receipt-vs-restore-purchases-in-ios/52162283#52162283

How to edit hexo theme hiero

Issue #494

Code

Remove max-width from source/css/style.styl

1
2
3
4
5
.outer
clearfix()
// max-width: (column-width + gutter-width) * columns + gutter-width
margin: 40px auto
padding: 0 gutter-width

Change font-size of code block from source/css/_partial/highlight.styl

1
2
3
4
5
6
7
8
9
10
11
$code-block
background: $highlight-background
margin: article-padding article-padding * 0
padding: 15px article-padding
border-style: solid
border-color: color-border
border-width: 1px 0
overflow: auto
color: $highlight-foreground
font-size: 1.0em;
line-height: 1em

Change font-size of article-header from source/css/_partial/article.styl

1
2
3
4
5
6
7
8
.article-header
h1
margin: 0 0 3px 0;
font-size: 26px;
line-height: 1.2;
a
color: #404040;
text-decoration: none;

Change font-size of text from source/css/_partial/article.styl

1
2
3
4
5
6
7
8
9
.article-entry
@extend $base-style
clearfix()
color: color-default
padding: 0
p, table
line-height: line-height
margin: line-height 0
font-size: 1.1em

How to use Firebase RemoteConfig

Issue #493

Declare in Podfile

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

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

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

final class RemoteConfigHandler {
let remoteConfig: RemoteConfig

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

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

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

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

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

self.remoteConfig.setDefaults(map)
}

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

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

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

How to apply translations to Localizable.strings

Issue #492

Suppose we have a base Localizable.strings

1
2
"open" = "Open";
"closed" = "Closed";

After sending that file for translations, we get translated versions.

1
2
"open" = "Åpen";
"closed" = "Stengt";

Searching and copy pasting these to our Localizable.strings is tedious and time consuming. We can write a script to apply that.

Remember that we need to be aware of smart and dump quotes

1
2
.replace(/\"/g, '')
.replace(/\"/g, '')
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
const fs = require('fs')

const originalFile = 'MyApp/Resources/nb.lproj/Localizable.strings'
const translationString = `
"open" = "Åpen";
"closed" = "Stengt";
`

class Translation {
constructor(key, value) {
this.key = key
this.value = value
}
}

Translation.make = function make(line) {
if (!line.endsWith(';')) {
return new Translation('', '')
}

const parts = line
.replace(';')
.split(" = ")

const key = parts[0]
.replace(/\"/g, '')
.replace(/\”/g, '')
const value = parts[1]
.replace(/\"/g, '')
.replace(/\”/g, '')
.replace('undefined', '')
return new Translation(key, value)
}

function main() {
const translations = translationString
.split(/\r?\n/)
.map((line) => { return Translation.make(line) })

apply(translations, originalFile)
}

function apply(translations, originalFile) {
try {
const originalData = fs.readFileSync(originalFile, 'utf8')
const originalLines = originalData.split(/\r?\n/)
const parsedLine = originalLines.map((originalLine) => {
const originalTranslation = Translation.make(originalLine)
const find = translations.find((translation) => { return translation.key === originalTranslation.key })
if (originalLine !== "" && find !== undefined && find.key !== "") {
return `"${find.key}" = "${find.value}";`
} else {
return originalLine
}
})

const parsedData = parsedLine.join('\n')
fs.writeFileSync(originalFile, parsedData, { overwrite: true })
} catch (err) {
console.error(err)
}
}

main()

How to get Binding via dollar prefix in SwiftUI

Issue #488

The dollar is not a prefix, it seems to be a generated code for property wrapper, and each kind of property wrapper can determine which value it return via this dollar sign

State and ObservedObject are popular property wrappers in SwiftUI

State

Read State

A persistent value of a given type, through which a view reads and monitors the value.

If we have a simple State, we can access its 3 forms

1
@State private var image: UIImage

and here is what

1
2
3
image // UIImage
_image // State<UIImage>
$image // Binding<UIImage>

Also, with State, we can access Binding via projectedValue

1
_image.projectedValue // Binding<UIImage>

ObservableObject

Read ObservedObject

For a simple ObservableObject, we can see its 3 forms

1
2
3
4
5
6
7
8
9
class ViewModel: ObservableObject {
var objectWillChange = ObservableObjectPublisher()

var image: UIImage? {
didSet {
objectWillChange.send()
}
}
}
1
@ObservedObject var viewModel: ViewModel = ViewModel()
1
2
3
viewModel // ViewModel
_viewModel // ObservedObject<ViewModel>
$viewModel // ObservedObject<ViewModel>.Wrapper

If we view the source code of ObservableObject, we can see its Wrapper which uses dynamicMemberLookup to provide Binding

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@propertyWrapper public struct ObservedObject<ObjectType> : DynamicProperty where ObjectType : ObservableObject {

/// A wrapper of the underlying `ObservableObject` that can create
/// `Binding`s to its properties using dynamic member lookup.
@dynamicMemberLookup public struct Wrapper {

/// Creates a `Binding` to a value semantic property of a
/// reference type.
///
/// If `Value` is not value semantic, the updating behavior for
/// any views that make use of the resulting `Binding` is
/// unspecified.
public subscript<Subject>(dynamicMember keyPath: ReferenceWritableKeyPath<ObjectType, Subject>) -> Binding<Subject> { get }
}

public init(initialValue: ObjectType)

public init(wrappedValue: ObjectType)

public var wrappedValue: ObjectType

public var projectedValue: ObservedObject<ObjectType>.Wrapper { get }
}

Derived State

If we have a struct with @State in a SwiftUI view, we can access its property as Binding. This derived state mechanism is done via Dynamic keypath member lookup feature of Swift 5.1

1
2
3
4
5
6
7
struct ViewModel {
var image: UIImage?
}

struct MainView: View {
@State private var viewModel: ViewModel = ViewModel()
}

Take a look at the interface of Binding

1
2
3
4
5
6
/// A value and a means to mutate it.
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
@propertyWrapper @dynamicMemberLookup public struct Binding<Value> {
/// Creates a new `Binding` focused on `Subject` using a key path.
public subscript<Subject>(dynamicMember keyPath: WritableKeyPath<Value, Subject>) -> Binding<Subject> { get }
}

So if use $viewModel.image we can access its property as Binding

1
2
3
viewModel.image // UIImage?
$viewModel.image // Binding<UIImage?>
$viewModel.$image // Value of type 'ViewModel' has no member '$image'

Conclusion

So now we know how to get Binding from State and ObservableObject, and the mysterious dollar sign. These are both convenient but confusing at first, but if we use it more, it will make more sense and hopefully we can learn to do the same for our own property wrappers

How to modify state from state in SwiftUI

Issue #487

In case we have to modify state when another state is known, we can encapsulate all those states in ObservableObject and use onReceive to check the state we want to act on.

See code Avengers

If we were to modify state from within body function call, we will get warnings

Modifying state during view update, this will cause undefined behavior.

This is similar to the warning when we change state inside render in React

For example, when we get an image, we want to do some logic based on that image and modify result state. Here we use var objectWillChange = ObservableObjectPublisher() to notify state change, and because onReceive requires Publisher, we use let imagePublisher = PassthroughSubject<UIImage, Never>()

Note that we use $ prefix from a variable to form Binding

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

class ViewModel: ObservableObject {
var objectWillChange = ObservableObjectPublisher()
let imagePublisher = PassthroughSubject<UIImage, Never>()

var image: UIImage? {
willSet {
objectWillChange.send()
if let image = image {
imagePublisher.send(image)
}
}
}

var isDetecting: Bool = false {
willSet {
objectWillChange.send()
}
}

var result: String? {
willSet {
objectWillChange.send()
}
}
}

struct MainView: View {
@State private var showImagePicker: Bool = false
@ObservedObject var viewModel: ViewModel = ViewModel()

private let detector = Detector()

var body: some View {
VStack {
makeImage()
.styleFit()

if viewModel.isDetecting {
ActivityIndicator(
isAnimating: $viewModel.isDetecting,
style: .large
)
}

makeResult()

Button(action: {
self.showImagePicker.toggle()
}, label: {
Text("Choose image")
})
.sheet(isPresented: $showImagePicker, content: {
ImagePicker(image: self.$viewModel.image, isPresented: self.$showImagePicker)
})
}
.onReceive(viewModel.imagePublisher, perform: { image in
self.detect(image: image)
})
}

private func makeImage() -> Image {
if let image = self.viewModel.image {
return Image(uiImage: image)
} else {
return Image("placeholder")
}
}

private func makeResult() -> Text {
if let result = viewModel.result {
return Text(result)
} else {
return Text("")
}
}

private func detect(image: UIImage) {
viewModel.isDetecting = true
try? detector.detect(image: image, completion: { result in
switch result {
case .success(let string):
self.viewModel.result = string
default:
self.viewModel.result = ""
}

self.viewModel.isDetecting = false
})
}
}

Use Published

See ObservableObject

By default an ObservableObject synthesizes an objectWillChange publisher that emits the changed value before any of its @Published properties changes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Contact: ObservableObject {
@Published var name: String
@Published var age: Int

init(name: String, age: Int) {
self.name = name
self.age = age
}

func haveBirthday() -> Int {
age += 1
return age
}
}

let john = Contact(name: "John Appleseed", age: 24)
john.objectWillChange.sink { _ in print("\(john.age) will change") }
print(john.haveBirthday())
// Prints "24 will change"
// Prints "25"

We should use @Published

1
2
3
4
5
6
7
class ViewModel: ObservableObject {
var objectWillChange = ObservableObjectPublisher()

@Published var image: UIImage?
@Published var isDetecting: Bool = false
@Published var result: String?
}

Note that we should not use objectWillChange as

By default an ObservableObject synthesizes an objectWillChange publisher that emits the changed value before any of its @Published properties changes.

1
2
3
4
5
.onReceive(viewModel.$image, perform: { image in
if let image = image {
self.detect(image: image)
}
})

We need to manually notify using objectWillChange !! Maybe this is a SwiftUI bug

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private func detect(image: UIImage) {
viewModel.isDetecting = true
try? detector.detect(image: image, completion: { result in
switch result {
case .success(let string):
self.viewModel.result = string
default:
self.viewModel.result = ""
}

self.viewModel.isDetecting = false
self.viewModel.objectWillChange.send()
})
}

If we remove the declaration of var objectWillChange = ObservableObjectPublisher(), then it works automatically

objectWillChange

Learn more about the history of objectWillChange

https://twitter.com/luka_bernardi/status/1155944329363349504?lang=no

In Beta 5 ObjectBinding is now defined in Combine as ObservableObject (the property wrapper is now @ObservedObject). There is also a new property wrapper @Published where we automatically synthesize the objectWillChange publisher and call it on willSet.

It’ll objectWillChange.send() in the property willSet it’s defined on.
It just removes the boilerplate that you had to write before but otherwise behaves the same.

State vs ObservedObject

If we were to use @State instead of @ObservedObject, it still compiles, but after we pick an image, which should change the image property of our viewModel, the view is not reloaded.

1
2
3
4
struct MainView: View {
@State private var showImagePicker: Bool = false
@State private var viewModel: ViewModel = ViewModel()
}

Note that we can’t use @Published inside struct

‘wrappedValue’ is unavailable: @Published is only available on properties of classes

@State is for internal usage within a view, and should use struct and primitive data structure. SwiftUI keeps @State property in a separate memory place to preserve it during many reload cycles.

@Observabled is meant for sharing reference objects across views

To to use @State we should use struct, and to use onReceive we should introduce another Publisher like imagePublisher

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
struct ViewModel {
var imagePublisher = PassthroughSubject<UIImage?, Never>()
var image: UIImage? {
didSet {
imagePublisher.send(image)
}
}
var isDetecting: Bool = false
var result: String?
}

struct MainView: View {
@State private var showImagePicker: Bool = false
@State private var viewModel: ViewModel = ViewModel()

private let detector = Detector()

var body: some View {
VStack {
makeImage()
.styleFit()

if viewModel.isDetecting {
ActivityIndicator(
isAnimating: $viewModel.isDetecting,
style: .large
)
}

makeResult()

Button(action: {
self.showImagePicker.toggle()
}, label: {
Text("Choose image")
})
.sheet(isPresented: $showImagePicker, content: {
ImagePicker(image: self.$viewModel.image, isPresented: self.$showImagePicker)
})
}
.onReceive(viewModel.imagePublisher, perform: { image in
if let image = image {
self.detect(image: image)
}
})
}
}

The dollar sign for State to access nested properties, like $viewModel.image is called derived Binding, and is achieved via Keypath member lookup feature of Swift 5.1.

Take a look at projectedValue: Binding<Value> from State and subscript<Subject>(dynamicMember keyPath from Binding

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
@propertyWrapper public struct State<Value> : DynamicProperty {

/// Initialize with the provided initial value.
public init(wrappedValue value: Value)

/// Initialize with the provided initial value.
public init(initialValue value: Value)

/// The current state value.
public var wrappedValue: Value { get nonmutating set }

/// Produces the binding referencing this state value
public var projectedValue: Binding<Value> { get }
}
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
/// A value and a means to mutate it.
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
@propertyWrapper @dynamicMemberLookup public struct Binding<Value> {

/// The transaction used for any changes to the binding's value.
public var transaction: Transaction

/// Initializes from functions to read and write the value.
public init(get: @escaping () -> Value, set: @escaping (Value) -> Void)

/// Initializes from functions to read and write the value.
public init(get: @escaping () -> Value, set: @escaping (Value, Transaction) -> Void)

/// Creates a binding with an immutable `value`.
public static func constant(_ value: Value) -> Binding<Value>

/// The value referenced by the binding. Assignments to the value
/// will be immediately visible on reading (assuming the binding
/// represents a mutable location), but the view changes they cause
/// may be processed asynchronously to the assignment.
public var wrappedValue: Value { get nonmutating set }

/// The binding value, as "unwrapped" by accessing `$foo` on a `@Binding` property.
public var projectedValue: Binding<Value> { get }

/// Creates a new `Binding` focused on `Subject` using a key path.
public subscript<Subject>(dynamicMember keyPath: WritableKeyPath<Value, Subject>) -> Binding<Subject> { get }
}

Read more

How to show loading indicator in SwiftUI

Issue #486

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

struct ActivityIndicator: UIViewRepresentable {
@Binding var isAnimating: Bool
let style: UIActivityIndicatorView.Style

func makeUIView(context: UIViewRepresentableContext<ActivityIndicator>) -> UIActivityIndicatorView {
return UIActivityIndicatorView(style: style)
}

func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext<ActivityIndicator>) {
isAnimating ? uiView.startAnimating() : uiView.stopAnimating()
}
}

struct ActivityIndicator_Previews: PreviewProvider {
static var previews: some View {
ActivityIndicator(isAnimating: .constant(true), style: .large)
}
}

How to show image picker in SwiftUI

Issue #485

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

First attempt, use Environment

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

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

My first attempt looks like below

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

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

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

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

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

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

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

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

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

Signatures

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

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

1
@Environment(\.presentationMode) private var presentationMode

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

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

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

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

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

How to use

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

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

Environment outside body

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

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

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

Second attempt, pass closure

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

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

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

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

Use Binding instead of Environment

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

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

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

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

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

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

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

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

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

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

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

How to use it

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

Pass ImagePicker to Coordinator

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

How to add monkey test to iOS apps

Issue #484

Use SwiftMonkey which adds random UITests gestures

Add to UITests target

1
2
3
4
target 'MyAppUITests' do
pod 'R.swift', '~> 5.0'
pod 'SwiftMonkey', '~> 2.1.0'
end

Troubleshooting

Failed to determine hittability of Button

Failed to determine hittability of Button: Unable to fetch parameterized attribute XC_kAXXCParameterizedAttributeConvertHostedViewPositionFromContext, remote interface does not have this capability.

This happens when using SwiftMonkey and somewhere in our code uses isHittable, so best to avoid that by having isolated monkey test only

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

class MonkeyTests: XCTestCase {
var app: XCUIApplication!

override func setUp() {
continueAfterFailure = false
app = XCUIApplication()
app.launch()
}

func testMonkey() {
let monkey = Monkey(frame: app.frame)
monkey.addDefaultUIAutomationActions()
monkey.addXCTestTapAlertAction(interval: 100, application: app)
monkey.monkeyAround()
}
}

Another workaround is possibly use addDefaultXCTestPublicActions other than addDefaultUIAutomationActions

UI Test Activity:

Assertion Failure: MonkeyXCTest.swift:33: Failed to get matching snapshots: Timed out while evaluating UI query.

This seems related to SwiftMonkey trying to snapshot. Workaround is to remove

1
monkey.addXCTestTapAlertAction(interval: 100, application: app)