Irbisa · Шпаргалка
SwiftUI: layout, идентичность и отрисовка
Протокол Layout, GeometryReader, PreferenceKey, идентичность view, Canvas, ленивые контейнеры
В этом горизонтальном
ScrollViewSpacerсхлопывается, и карточки стоят вплотную: что предлагаетScrollViewи как это исправить?ScrollView(.horizontal) { HStack { CardView(card: first) Spacer() CardView(card: second) }}Когда стоит использовать
GeometryReaderи что он делает с layout вокруг себя?// Wrong: the reader eats the leftover height and pushes Footer downVStack { Title() GeometryReader { proxy in Chart(width: proxy.size.width) // drawn at the top-leading corner }…Что такое
PreferenceKeyи как ребёнок передаёт значение вверх своим предкам?struct WidthKey: PreferenceKey { static let defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = max(value, nextValue()) }}…Как
ViewThatFitsвыбирает, какого ребёнка показать, и почему это часто лучше проверкиhorizontalSizeClass?ViewThatFits(in: .horizontal) { // 1. labels with icons HStack { ForEach(actions) { Label($0.title, systemImage: $0.icon) } } // 2. icons only…Как обернуть UIKit-вью в
UIViewRepresentable, встроить SwiftUI в UIKit черезUIHostingControllerи как каждая сторона узнаёт размер другой?// UIKit inside SwiftUI: a multi-line label that wraps to the width SwiftUI offersstruct WrappingLabel: UIViewRepresentable { let text: String func makeUIView(context: Context) -> UILabel { let label = UILabel()…Как работают alignment guides и как с помощью собственного выравнивания выстроить в линию вью из разных строк?
// Badge level with the first line of a wrapping titleHStack(alignment: .firstTextBaseline) { Text(article.title) Badge("NEW")}…Что такое identity вью в SwiftUI (структурная и явная) и что происходит, когда identity вью меняется?
Пользователь выбирает другой элемент в сайдбаре, а этот редактор по-прежнему показывает заголовок предыдущего: почему и как это исправить?
struct DetailEditor: View { let item: Item @State private var draft: String init(item: Item) { self.item = item _draft = State(initialValue: item.title) } var body: some View { TextField("Title", text: $draft) }}SwiftUI предупреждает, что ID встречается в
ForEachнесколько раз, и строки начинают путать состояние: что сломалось и где это чинить?Каждый раз, когда
isHighlightedменяется, счётчик в этой строке сбрасывается в ноль: что не так с этим хелпером условного модификатора?extension View { @ViewBuilder func `if`<T: View>(_ condition: Bool, transform: (Self) -> T) -> some View { if condition { transform(self) } else { self } }} struct Row: View { let isHighlighted: Bool var body: some View { Counter() // holds @State private var count = 0 .if(isHighlighted) { $0.foregroundStyle(.red) } }}Найдите баг: все подписи в форме обрезаны до ширины «Phone», и «Email address» усечено, хотя ширину должна задавать самая широкая подпись.
struct LabelWidthKey: PreferenceKey { static let defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = nextValue() }}struct SettingsForm: View { @State private var labelWidth: CGFloat = 0 let titles = ["Name", "Email address", "Phone"] var body: some View { VStack(alignment: .leading) { ForEach(titles, id: \.self) { title in Text(title) .background(GeometryReader { Color.clear.preference(key: LabelWidthKey.self, value: $0.size.width) }) .frame(width: labelWidth == 0 ? nil : labelWidth, alignment: .leading) } } .onPreferenceChange(LabelWidthKey.self) { labelWidth = $0 } }}Почему
onAppearвLazyVStackсрабатывает раньше, чем строка появилась на экране, и как сделать пагинацию, которая это переживёт?@MainActor @Observablefinal class Feed { private(set) var items: [Item] = [] private var nextPage = 0 private var isLoading = false…Почему
proxy.scrollTo(id), вызванный сразу после добавления сообщения, часто промахивается, и что можно использовать вместоScrollViewReaderна iOS 17–18?struct ChatView: View { @State private var messages: [Message] = [] @State private var position = ScrollPosition(idType: Message.ID.self) // iOS 18 var body: some View { ScrollView {…Что ломается, если повесить
.ignoresSafeArea()на корень экрана, и как дать дойти до краёв только фону?struct ProfileScreen: View { @State private var name = "" var body: some View { Form { TextField("Name", text: $name)…Как построить через протокол
Layoutраскладку тегов с переносом на новые строки и что обязаны делатьsizeThatFitsиplaceSubviews?struct FlowLayout: Layout { var spacing: CGFloat = 8 func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { let frames = arrange(subviews, maxWidth: proposal.width ?? .infinity) // nil: one row return CGSize(width: frames.map(\.maxX).max() ?? 0, height: frames.map(\.maxY).max() ?? 0)…Найдите баг: этот layout тегов рисует теги в левом верхнем углу экрана, а не под заголовком в
VStack.struct TagFlow: Layout { var spacing: CGFloat = 8 func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { measure(subviews, maxWidth: proposal.width ?? .infinity) // correct: sums the rows, no origin involved } func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { var x: CGFloat = 0, y: CGFloat = 0, rowHeight: CGFloat = 0 for subview in subviews { let size = subview.sizeThatFits(.unspecified) if x + size.width > bounds.width, x > 0 { x = 0; y += rowHeight + spacing; rowHeight = 0 } subview.place(at: CGPoint(x: x, y: y), proposal: ProposedViewSize(size)) x += size.width + spacing rowHeight = max(rowHeight, size.height) } }}Что делают
compositingGroup()иdrawingGroup()и когда каждый из них помогает, а когда вредит?// wrong: the shadow is drawn under every childVStack { Text("Title"); Image(systemName: "star") } .shadow(radius: 6) // right: children compose first, one shadow for the resultVStack { Text("Title"); Image(systemName: "star") }…Волна из 2000 вью
Rectangleтеряет кадры: когда переходить наCanvasи чем за это придётся заплатить?struct Waveform: View { let samples: [Float] var body: some View { TimelineView(.animation) { timeline in Canvas { context, size in…Контейнер
Cardдолжен ставить разделитель между детьми, но переданныйForEachсчитается одной вью: как добраться до настоящих детей?extension ContainerValues { @Entry var isPinned: Bool = false} struct Card<Content: View>: View { @ViewBuilder var content: Content…bodyдешёвые, но инструмент SwiftUI показывает время в layout, и экран всё равно подтормаживает: что делает layout дорогим?Что такое AttributeGraph и как SwiftUI решает, какой
bodyвыполнить заново после изменения состояния?struct PriceChart: View, Equatable { let points: [PricePoint] let onSelect: (PricePoint) -> Void // a closure cannot be compared // nonisolated: Equatable is not main-actor isolated nonisolated static func == (lhs: Self, rhs: Self) -> Bool {…