This commit is contained in:
Lukas Capkovic 2022-05-23 13:59:42 -07:00 коммит произвёл GitHub
Родитель 5372d4dfb4
Коммит 12f358e32e
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
2 изменённых файлов: 125 добавлений и 1 удалений

Просмотреть файл

@ -5,10 +5,11 @@ This guide contains best practices for building great experiences and writing su
## Table of Contents
* [iOS Image File Format](iOSImageFileFormat.md)
* [Keyboard Focus](KeyboardFocus.md)
* [Layout](Layout.md)
* [Storyboards and Xibs](StoryboardsAndXibs.md)
* [SwiftUI](SwiftUI.md)
* [View Controllers](ViewControllers.md)
* [Keyboard Focus](KeyboardFocus.md)
## Contributing

123
SwiftUI.md Normal file
Просмотреть файл

@ -0,0 +1,123 @@
# SwiftUI
## GeometryReader
### Convention
Avoid using [`GeometryReaders`](https://developer.apple.com/documentation/swiftui/geometryreader) in your main view tree. To measure views, place `GeometryReaders` inside of a `.background` or `.overlay` and communicate the size up through a `Preference` or other methods.
### Rationale
`GeometryReaders` always fill all of their proposed size, essentially overriding the layout behavior of the views they wrap. They also override the children's ideal size.
Usage of `.background` or `.overlay` is recommended because the content of these modifiers doesn't affect the layout of the parent view, so this limits the GR's impact while still allowing you to measure the parent view final size (note this can be different from the proposed size).
### Example
```swift
// Good: Measure final size of Text without affecting its layout
Text("Sample text")
.background { // Size proposed to this will be the final size of the Text view
GeometryReader { proxy in
Color.clear.preference(key: SizePreferenceKey.self, value: proxy.size)
}
}
.onPreferenceChange(SizePreferenceKey.self) { size in
// Do things with the measured size
}
private struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
```
```swift
// Bad: We measure size proposed to the Text view, but the GeometryReader won't fit the text.
GeometryReader { proxy in
Text("Sample text")
}
```
### Exceptions
There are cases where you may need to read the size proposed to a view instead of the final size (for example to do custom proportional layout). In these cases, first consider if a combination of stacks, spacers and frames can be used instead.
If you absolutely need the `GeometryReader`, expanding to fill the dimension you want to measure is unavoidable.
#### Example 1
Let's say you want to ensure a view's width is always 0.65x of the proposed width and the height is self sized.
Wrap a `GeometryReader` in a frame, constraining the dimension you don't care about.
```swift
GeometryReader { proxy in
...
}
.frame(height: 0) // The GeometryReader won't fill all proposed height
```
Note that if you place your content inside of this `GeometryReader`, as far as the `GeometryReader`'s parent is concerned, the height will always be 0. This can cause layout issues because the inner view will render out of bounds.
```swift
GeometryReader { proxy in
Rectangle()
// The Rectangle will take 20pts, but this height will be ignored by the layout system.
.frame(width: proxy.size.width * 0.65, height: 20)
}
.frame(height: 0)
```
This is caused by the `.frame` we used to constrain the `GeometryReader` to 0 height. To get the benefits of a height-constrained `GeometryReader` and a correctly sized content view, you can place the `GeometryReader` in a `ZStack` and bubble up its measurement. Combined with the previous method, this means:
- you measure the proposed width
- you avoid taking up all of the proposed height
- the height proposed to the content view is untouched
- the height returned by the content view is untouched
Note that a `ZStack` will match the size of its largest child in each dimension, so it will still fill all width due to the contained `GeometryReader`.
```swift
// The Rectangle will have a width proportional to the proposed width without taking up all the proposed height.
// This is a two pass layout
@State var availableWidth: CGFloat = 0
ZStack {
GeometryReader { proxy in
Color.clear.preference(key: WidthPreferenceKey.self,
value: proxy.size.width)
}
.frame(height: 0)
.onPreferenceChange(WidthPreferenceKey.self) { newWidth in
availableWidth = newWidth
}
Rectangle()
.frame(width: availableWidth * 0.65, height: 20)
}
```
#### Example 2
If your inner view is flexible and wants to span the proposed height anyways, the `ZStack` trick is unnecessary.
You will find that the `GeometryReader` aligns the smaller inner view based on undocumented logic (top-leading as of 5/20/22). To make the alignment explicit and customizable, wrap your view in essentially a passthrough frame with an explicit alignment parameter.
```swift
// This view will span the proposed width and height
// The Rectangle will span the proposed height and take 0.65x of the proposed width.
// It will be centered in the proposed width.
GeometryReader { proxy in
Rectangle()
.frame(width: proxy.size.width * 0.65)
// This frame has the same sizing behavior as the GeometryReader, but allows you to change the alignment.
.frame(width: proxy.size.width,
height: proxy.size.height,
alignment: .center)
}
```
## Multiline text
### Convention
Use native `Text` instead of a `UIViewRepresentable` containing a `UILabel` whenever possible.
### Rationale
SwiftUI-UIKit layout bridging is heavily dependent on `intrinsicContentSize`. `UILabel` calculates this size in a special way, taking its `preferredMaxLayoutWidth` into consideration to determine how many lines the label will need. In an Auto Layout environment this property is set automatically, but it won't be set by SwiftUI. This results in incorrect sizing for multiline `UILabels` when used in SwiftUI. The native `Text` doesn't suffer from these problems and takes the proposed width into consideration during a layout pass.
### Exceptions
There are cases where the use of `UILabel` is unavoidable, for example attributed text, which has only been supported in SwiftUI since iOS 15. In these cases you need to ensure `preferredMaxLayoutWidth` is set to the correct value. There are multiple methods of doing this:
- in static width cases, you can directly set the property in your representable
- if width is dynamic, you'll need to pass it down to your representable from the SwiftUI context
- if the width is manually computed, you can pass it as a `@Binding`, making sure to update the `UILabel` in the representable's `updateUIView` function.
- if the width follows from a SwiftUI layout pass, you might need to use a `GeometryReader` to measure the relevant views. See the `GeometryReader` sections for details.