Irbisa · Шпаргалка

Основы SwiftUI

Протокол View, модификаторы, контейнеры, навигация, списки

Junior-разработчик · 25 вопросов

  1. 01

    Что такое SwiftUI и в чём его плюсы и минусы по сравнению с UIKit?

    SwiftUI представляет собой декларативный UI-фреймворк Apple: вы описываете, как выглядит экран при текущем состоянии, а фреймворк сам решает, какие изменения применить.

  2. 02

    Что значит «декларативный UI» и почему view в SwiftUI сделаны структурами, а не классами?

    Декларативный означает, что вы описываете результат (как выглядит экран при данном состоянии), а обновления выполняет фреймворк.

  3. 03

    Что такое протокол View, что такое body и что означает some View?

    Протоколу View соответствует каждая view в SwiftUI, и его единственным требованием служит вычисляемое свойство body, возвращающее другие view.

    struct ProfileHeader: View {  let name: String  let isPro: Bool   // The real type is VStack<TupleView<(Text, Text?)>>; `some View` hides it  var body: some View {…
  4. 04

    Что делают @main, App и Scene в SwiftUI-приложении?

    @main отмечает точку входа в приложение, протокол App описывает приложение целиком, а его body возвращает одну или несколько сцен, то есть контейнеров вроде WindowGroup, которые система превращает в окна.

    @mainstruct NotesApp: App {  @State private var store = NotesStore()   // @Observable model owned by the app  @UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate  @Environment(\.scenePhase) private var scenePhase…
  5. 05

    Как VStack, HStack и ZStack раскладывают дочерние view и когда вместо них нужен LazyVStack?

    VStack ставит дочерние view сверху вниз, HStack ставит их от leading к trailing, а ZStack накладывает их слоями от заднего к переднему.

    VStack(alignment: .leading, spacing: 4) {  Text("Title").font(.headline)  Text("Subtitle").foregroundStyle(.secondary)} HStack {…
  6. 06

    Как выглядит каждый из трёх бейджей и почему перестановка двух модификаторов меняет результат?

    // Badge AText("Sale")  .padding()  .background(.red) // Badge BText("Sale")  .background(.red)  .padding() // Badge CText("Sale")  .background(.red)  .frame(width: 200)

    Бейдж A получается красным прямоугольником с текстом внутри стандартного отступа, B выглядит как узкая красная полоса за текстом с невидимым отступом вокруг, а C красный только за текстом внутри более широкой пустой рамки.

  7. 07

    Как сделать переиспользуемый стиль в SwiftUI: свой ViewModifier или ButtonStyle?

    ViewModifier нужен, чтобы собрать набор модификаторов для любой view, например вид карточки, а ButtonStyle нужен, чтобы стилизовать кнопки, потому что он дополнительно получает состояние нажатия и роль кнопки.

    struct CardStyle: ViewModifier {  func body(content: Content) -> some View {    content      .padding()      .background(.regularMaterial, in: .rect(cornerRadius: 16))  }…
  8. 08

    Вы растянули Text через .frame(maxWidth: .infinity), а он остался по центру. Чем различаются выравнивания в SwiftUI?

    .frame по умолчанию центрирует дочернюю view, поэтому нужен .frame(maxWidth: .infinity, alignment: .leading).

    // wrong: the frame stretches, the text stays in its centreText("Title").frame(maxWidth: .infinity) // right: tell the frame where to put its childText("Title").frame(maxWidth: .infinity, alignment: .leading)…
  9. 09

    Картинка Image из бандла игнорирует .frame, а .scaledToFill() вылезает на соседей: в каком порядке ставить модификаторы и чего не делает за вас AsyncImage?

    Image сохраняет свой натуральный размер, пока вы не вызовете .resizable(), а .scaledToFill() рисует за пределами рамки, если её не обрезать.

    // wrong: a bundled image ignores the proposed size until it is resizableImage("cover").frame(width: 120, height: 120)   // full size, overflowing // right: resizable, content mode, frame, then clip what .fill spillsImage("cover")  .resizable()…
  10. 10

    Как собрать List с ForEach и зачем каждой строке стабильный id?

    List служит прокручиваемым контейнером в стиле платформы, а ForEach создаёт по строке на каждый элемент коллекции.

    struct Todo: Identifiable {  let id: UUID          // stable: stored on the model, not generated in body  var title: String  var isDone = false}…
  11. 11

    Как собрать форму в SwiftUI с TextField, SecureField, Toggle и Picker?

    Оберните контролы в Form, который даёт им сгруппированный вид как в настройках, и привяжите каждый контрол к состоянию через $, например TextField("Email", text: $email).

    struct SignUpForm: View {  @State private var email = ""  @State private var password = ""  @State private var plan: Plan = .free  @State private var wantsNews = false…
  12. 12

    Как показать sheet или alert в SwiftUI и когда использовать форму item: вместо isPresented:?

    Показ экрана задаётся модификатором, привязанным к состоянию: установите Bool или опциональное значение, и SwiftUI покажет sheet или alert; сбросьте, и экран закроется.

    struct InboxView: View {  @State private var editing: Message?          // nil means no sheet  @State private var pendingDelete: Message?   var body: some View {    List(messages) { message in…
  13. 13

    Когда использовать LazyVGrid, а когда Grid, и как сделать, чтобы число колонок зависело от ширины экрана?

    LazyVGrid раскладывает коллекцию по колонкам и строит ячейки лениво, что подходит для галереи; Grid, неленивая таблица, строки которой делят ширину колонок, подходит для формы или таблицы сравнения.

    // Gallery: as many 120-pt columns as fit, cells built lazilyScrollView {  LazyVGrid(columns: [GridItem(.adaptive(minimum: 120), spacing: 8)], spacing: 8) {    ForEach(photos) { photo in      Thumbnail(photo).frame(height: 120)    }…
  14. 14

    Как писать превью SwiftUI для view с несколькими состояниями или с зависимостью от сервиса?

    Используйте макрос #Preview (по одному именованному превью на каждое состояние, способное сломать вёрстку) и подавайте в него тестовые данные и фейковые сервисы, чтобы превью никогда не ходило в сеть или в настоящую базу.

    #Preview("Default") {  ProfileCard(user: .preview)} #Preview("Long bio, largest text") {  ProfileCard(user: User(id: UUID(), name: "Alice",…
  15. 15

    Когда использовать List, а когда ScrollView с LazyVStack?

    List подходит для длинных однотипных данных: он переиспользует строки и бесплатно даёт разделители, swipe-действия, выделение и режим редактирования.

  16. 16

    Как SwiftUI определяет размер view: за кем последнее слово, за родителем или за ребёнком?

    Layout устроен как переговоры: родитель предлагает размер, дочерняя view выбирает свой размер, а затем родитель её размещает.

    // .frame proposes a size and positions whatever the child returnsText(longString).frame(width: 100)            // Text wraps into 100 pt // fixedSize asks for the ideal size: stop the vertical truncationText(paragraph)  .fixedSize(horizontal: false, vertical: true)…
  17. 17

    Как работает NavigationStack и как переходить между экранами из кода или по диплинку?

    NavigationStack хранит свои экраны как путь (path) из значений: NavigationLink(value:) добавляет значение, .navigationDestination(for:) превращает значение этого типа в экран, а кнопка «Назад» удаляет последнее.

    enum Route: Hashable {  case profile(userID: String)  case settings} struct RootView: View {…
  18. 18

    Тап по строке этого списка ничего не делает, а Xcode пишет предупреждение о навигации: в чём ошибка?

    struct ShopView: View {  let products: [Product]   var body: some View {    NavigationStack {      List(products) { product in        NavigationLink(product.name, value: product)          .navigationDestination(for: Product.self) { product in            ProductDetail(product: product)          }      }      .navigationTitle("Shop")    }  }}

    .navigationDestination стоит внутри строк List.

  19. 19

    Что выведет этот код при появлении экрана и при каждом тапе и что это говорит об init, body и onAppear?

    struct Child: View {  init() { print("init") }  var body: some View {    let _ = print("body")    Text("Child").onAppear { print("appear") }  }} struct Parent: View {  @State private var count = 0  var body: some View {    VStack { Text("Taps: \(count)"); Child(); Button("Tap") { count += 1 } }  }}

    В обычном запуске при первом появлении выводится init, body, appear; после каждого тапа выводится только init.

  20. 20

    Найдите баг: при каждом возвращении на этот экран секундомер считает всё быстрее и продолжает тикать, пока поверх открыт другой экран.

    struct StopwatchView: View {  @State private var seconds = 0   var body: some View {    Text("\(seconds) s")      .font(.largeTitle.monospacedDigit())      .onAppear {        Task {          do {            while true {              try await Task.sleep(for: .seconds(1))              seconds += 1            }          } catch {}        }      }  }}

    Task, запущенный в .onAppear, не привязан к view, и его никто не отменяет: каждое возвращение на экран запускает ещё один бесконечный цикл, и все они продолжают увеличивать seconds, даже пока этот экран закрыт другим.

  21. 21

    Что @ViewBuilder делает под капотом и почему возвращать AnyView считается плохой привычкой?

    @ViewBuilder работает как result builder, который сворачивает список выражений в один вложенный generic-тип view; поэтому в body можно написать несколько view и if, не выписывая этот тип вручную.

    // Your own container that takes a builderstruct Card<Content: View>: View {  @ViewBuilder var content: Content   var body: some View {    VStack(alignment: .leading, spacing: 8) { content }…
  22. 22

    Чем withAnimation отличается от модификатора .animation и почему одноаргументный .animation(_:) объявили устаревшим?

    withAnimation анимирует все изменения, вызванные мутацией состояния внутри его замыкания, где бы ни находились затронутые view; модификатор .animation(_:value:) анимирует реакцию одной view на одно конкретное значение.

    struct Panel: View {  @State private var isExpanded = false  @State private var count = 0   var body: some View {    VStack {…
  23. 23

    В строке списка есть кнопка «звезда» и кнопка удаления, и тап по любой запускает обе плюс навигацию строки: что происходит?

    Внутри строки List кнопки со стилем по умолчанию используют всю строку как область нажатия, поэтому один тап срабатывает у всех кнопок строки.

    // wrong: .automatic buttons inherit the row's tap regionList(messages) { message in  NavigationLink(value: message) {    HStack {      Text(message.title)      Spacer()…
  24. 24

    После сохранения на четвёртом экране стека нужно вернуться в корень, а тот же флоу работает и внутри sheet: как это построить?

    Возврат в корень сводится к одному изменению данных (очистке массива, управляющего NavigationStack), поэтому глубокому экрану нужен доступ к этому path, ведь @Environment(\.dismiss) откатывает только один уровень.

    @Observable @MainActorfinal class Router { var path: [Route] = [] } struct RootView: View {  @State private var router = Router()  @State private var sheetRouter = Router()…
  25. 25

    Найдите баг: при выборе другой статьи заголовок обновляется, а этот web view продолжает показывать первую страницу.

    struct WebView: UIViewRepresentable {  let url: URL   func makeUIView(context: Context) -> WKWebView {    let webView = WKWebView()    webView.load(URLRequest(url: url))    return webView  }   func updateUIView(_ webView: WKWebView, context: Context) {}} // In the parent: WebView(url: selectedArticle.url)

    updateUIView пустой, поэтому новый url так и не доходит до WKWebView.