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 use array of strings in ForEach in SwiftUI

Issue #483

Every item in list must be uniquely identifiable

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

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

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

How to make multiline Text in SwiftUI in watchOS

Issue #482

lineLimit does not seem to work, use fixedSize instead

Fixes this view at its ideal size.

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

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

How to reload data without using onAppear in SwiftUI in watchOS

Issue #468

From onAppeear

Adds an action to perform when the view appears.

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

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

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

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

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

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

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

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

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

Inside our ObservableObject, we need to trigger changes notification

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

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

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

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

How to use EnvironmentObject in SwiftUI for watchOS

Issue #467

Declare top dependencies in ExtensionDelegate

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

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

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

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

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

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

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

Fatal error: No ObservableObject of type SomeType found
A View.environmentObject(_:) for StoreContainer.Type may be missing as an ancestor of this view

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct MainView: View {
@EnvironmentObject var storeContainer: StoreContainer

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

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

How to make container view in SwiftUI

Issue #450

Following the signatures of ScrollView and Group, we can create our own container

1
2
3
4
5
6
public struct ScrollView<Content> : View where Content : View {

/// The content of the scroll view.
public var content: Content

}
1
2
3
4
5
6
7
8
9
10
extension Group : View where Content : View {

/// The type of view representing the body of this view.
///
/// When you create a custom view, Swift infers this type from your
/// implementation of the required `body` property.
public typealias Body = Never

@inlinable public init(@ViewBuilder content: () -> Content)
}

For example, below is a FullWidth that encapsulate a child element and make it full width

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

struct FullWidth<Content>: View where Content: View {
let content: Content

var body: some View {
GeometryReader { geometry in
self.content
.frame(width: geometry.size.width, height: geometry.size.width, alignment: .center)
.padding(.bottom, geometry.size.width)
}
}

@inlinable public init(@ViewBuilder content: () -> Content) {
self.content = content()
}
}

struct FullWidth_Previews: PreviewProvider {
static var previews: some View {
FullWidth {
Text("")
}
}
}

One problem with GeometryReader is that the size of the children can’t affect the size of its container. We need to apply Geometry at the root level

1
2
3
4
5
6
7
8
9
10
11
12
var body: some View {
GeometryReader { geometry in
ScrollView {
VStack(alignment: .leading, spacing: 8) {
Text("title")
RemoteImage(url: self.item.url)
.frame(width: geometry.size.width, height: geometry.size.height, alignment: .center)
Text("footer")
}
}
}
}

How to show web content as QR code in SwiftUI in watchOS

Issue #449

WatchKit does not have Web component, despite the fact that we can view web content

A workaround is to show url as QR code

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

struct QRCodeView: View {
let title: String
let url: URL

var body: some View {
GeometryReader { geometry in
VStack {
self.makeImage(size: geometry.size)
.padding(.top, 10)
Text("Scan to open")
.font(.system(.footnote))
}.navigationBarTitle(self.title)
}
}

private func makeImage(size: CGSize) -> some View {
let value = size.height - 30
return RemoteImage(url: self.url)
.frame(width: value, height: value, alignment: .center)
}
}

How to load remote image in SwiftUI

Issue #448

Use ObservableObject and onReceive to receive event. URLSession.dataTask reports in background queue, so need to .receive(on: RunLoop.main) to receive events on main queue.

For better dependency injection, need to use ImageLoader from Environment

There should be a way to propagate event from Publisher to another Publisher, for now we use sink

ImageLoader.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 Combine
import WatchKit

class ImageLoader: ObservableObject {
private var cancellable: AnyCancellable?
let objectWillChange = PassthroughSubject<UIImage?, Never>()

func load(url: URL) {
self.cancellable = URLSession.shared
.dataTaskPublisher(for: url)
.map({ $0.data })
.eraseToAnyPublisher()
.receive(on: RunLoop.main)
.map({ UIImage(data: $0) })
.replaceError(with: nil)
.sink(receiveValue: { image in
self.objectWillChange.send(image)
})
}

func cancel() {
cancellable?.cancel()
}
}

RemoteImage.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
25
26
27
28
29
30
31
32
33
34
35
import SwiftUI
import WatchKit

struct RemoteImage: View {
let url: URL
let imageLoader = ImageLoader()
@State var image: UIImage? = nil

var body: some View {
Group {
makeContent()
}
.onReceive(imageLoader.objectWillChange, perform: { image in
self.image = image
})
.onAppear(perform: {
self.imageLoader.load(url: self.url)
})
.onDisappear(perform: {
self.imageLoader.cancel()
})
}

private func makeContent() -> some View {
if let image = image {
return AnyView(
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
)
} else {
return AnyView(Text("😢"))
}
}
}

How to do navigation in SwiftUI in watchOS

Issue #447

NavigationView is not available on WatchKit, but we can just use NavigationLink

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
List(services.map({ AnyService($0) })) { anyService in
NavigationLink(destination:
ItemsView(service: anyService.service)
.navigationBarTitle(anyService.service.name)
.onDisappear(perform: {
anyService.service.requestCancellable?.cancel()
})
) {
HStack {
Image(anyService.service.name)
.resizable()
.frame(width: 30, height: 30, alignment: .leading)
Text(anyService.service.name)
}
}
}

Adding NavigationLink to a View adds a round shadow cropping effect, which is usually not want we want.

But we shouldn’t wrap Button as Button handles its own touch event, plus it has double shadow effect.

1
2
3
4
5
6
7
NavigationLink(destination:
QRCodeView(title: item.title, url: item.url)
) {
Button(action: {}) {
Text("Open")
}
}

Just use Text and it’s good to go

1
2
3
4
5
NavigationLink(destination:
QRCodeView(title: item.title, url: item.url)
) {
Text("Open")
}

How to use protocol in List in SwiftUI

Issue #446

Suppose we have Service protocol, and want to use in List

1
2
3
protocol Service {
var name: String { get }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct MainView: View {
let services = [
Car()
Plane()
]

var body: some View {
List(services) { service in
HStack {
Image(service.name)
Text(service.name)
}
}
}
}

This is not possible because item in List needs to conform to Identifiable

Protocol type ‘Service’ cannot conform to ‘Identifiable’ because only concrete types can conform to protocols

Type eraser

In the same way that SwiftUI uses type eraser, for example AnyView, we can introduce AnyService to work around this

1
2
3
4
5
6
7
var body: some View {
if useImage {
return AnyView(Image("my image"))
} else {
return AnyView(Text("my text"))
}
}

Make AnyService conform to Identifiable

1
2
3
4
5
6
7
8
9
struct AnyService: Identifiable {
let id: String
let service: Service

init(_ service: Service) {
self.service = service
self.id = service.name
}
}

Then in our View, we just need to declare services wrapped inside AnyService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct MainView: View {
let services = [
AnyService(Car()),
AnyService(Plane())
]

var body: some View {
List(services) { anyService in
HStack {
Image(anyService.service.name)
Text(anyService.service.name)
}
}
}
}

A bit refactoring, we can just declare normal services and map them

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct MainView: View {
let services: [Service] = [
Car(),
Plane()
]

var body: some View {
List(services.map({ AnyService($0 })) { anyService in
HStack {
Image(anyService.service.name)
Text(anyService.service.name)
}
}
}
}