综合技术

SwiftUI By Example 笔记

微信扫一扫,分享到朋友圈

SwiftUI By Example 笔记
0

原文为 @twostrawsSwiftUI By Example ,就是简单记录一些主要内容。

Introduction

A brief explanation of the basics of SwiftUI

What is SwiftUI?

  1. 声明式对比命令式,能够更好地处理不同状态的 UI
  2. 跨平台:支持 iOS, macOS, tvOS, watchOS

SwiftUI vs Interface Builder and storyboards

  1. IB 难以阅读和修改
  2. IB 难以看出修改了内容
  3. IB 和 Swift 交互不够友好,充满了 Objective-C 设计
  4. SwiftUI 是个近支持 Swift 的库,这样才可以充分利用 Swift 的特性

Anyway, we’ll get onto exactly how SwiftUI works soon. For now, the least you need to know is that SwiftUI fixes many problems people had with the old Swift + Interface Builder approach:

  • We no longer have to argue about programmatic or storyboard-based design, because SwiftUI gives us both at the same time.
  • We no longer have to worry about creating source control problems when committing user interface work, because code is much easier to read and manage than storyboard XML.
  • We no longer need to worry so much about stringly typed APIs – there are still some, but significantly fewer.
  • We no longer need to worry about calling functions that don’t exist, because our user interface gets checked by the Swift compiler.

So, I hope you’ll agree there are lots of benefits to be had from moving to SwiftUI!

Frequently asked questions about SwiftUI

Does SwiftUI replace UIKit?

No. Many parts of SwiftUI directly build on top of existing UIKit components, such as UITableView. Of course, many other parts don’t – they are new controls rendered by SwiftUI and not UIKit.

But the point isn’t to what extent UIKit is involved. Instead, the point is that we don’t care . SwiftUI more or less completely masks UIKit’s behavior, so if you write your app for SwiftUI and Apple replaces UIKit with a singing elephant in two years you don’t have to care – as long as Apple makes the elephant compatible with the same methods and properties that UIKit exposed to SwiftUI, your code doesn’t change.

Is SwiftUI fast?

SwiftUI is screamingly fast – in all my tests so far it seems to outpace UIKit. Having spoken to the team who made it I’m starting to get an idea why: first, they aggressively flatten their layer hierarchy so the system has to do less drawing, but second many operations bypass Core Animation entirely and go straight to Metal for extra speed.

So, yes: SwiftUI is incredibly fast, and all without us having to do any extra work.

How to follow this quick start guide

最好顺序阅读该教程

Migrating from UIKit to SwiftUI

如果你用过 UIKit,不难发现 SwiftUI 就是把 UI 前缀去掉既是对应的组件。

Here’s a list to get you started, with UIKit class names followed by SwiftUI names:

  • UITableView : List
  • UICollectionView : No SwiftUI equivalent
  • UILabel : Text
  • UITextField : TextField
  • UITextField with isSecureTextEntry set to true: SecureField
  • UITextView : No SwiftUI equivalent
  • UISwitch : Toggle
  • UISlider : Slider
  • UIButton : Button
  • UINavigationController : NavigationView
  • UIAlertController with style .alert : Alert
  • UIAlertController with style .actionSheet : ActionSheet
  • UIStackView with horizontal axis: HStack
  • UIStackView with vertical axis: VStack
  • UIImageView : Image
  • UISegmentedControl : SegmentedControl
  • UIStepper : Stepper
  • UIDatePicker : DatePicker
  • NSAttributedString : Incompatible with SwiftUI; use Text instead.

Text and images

Getting started with basic controls

What’s in the basic template?

SceneDelegate.swift is responsible for managing the way your app is shown.

let window = UIWindow(frame: UIScreen.main.bounds)
window.rootViewController = UIHostingController(rootView: ContentView())
self.window = window
window.makeKeyAndVisible()

Open ContentView.swift and let’s look at some actual SwiftUI code.

import SwiftUI

struct ContentView : View {
    var body: some View {
        Text("Hello World")
    }
}

#if DEBUG
struct ContentView_Previews : PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
#endif

First, notice how ContentView is a struct.

Second, ContentView conforms to the View protocol.

第三, body 的返回类型是 some viewsome 关键字是 Swift 5.1 中的新关键字,是名为 opaque return types 的功能的一部分,在这种情况下,就是字面意思:”这将返回某种 View ,但 SwiftUI 不需要知道(或关心)什么。”

Finally, below ContentView is a similar-but-different struct called ContentView_Previews .

How to create static labels with a Text view

Text("Hello World")
    .lineLimit(3)
Text("This is an extremely long string that will never fit even the widest of Phones")
    .truncationMode(.middle)

How to style text views with fonts, colors, line spacing, and more

Text("This is an extremely long string that will never fit even the widest of Phones")
        .truncationMode(.middle)
        .font(Font.body)
        .foregroundColor(Color.green)
        .background(Color.gray)
        .lineLimit(nil)
        .lineSpacing(30)

How to format text inside text views

struct ContentView: View {
    static let taskDateFormat: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateStyle = .long
        return formatter
    }()

    var dueDate = Date()

    var body: some View {
        Text("Task due date: (dueDate, formatter: Self.taskDateFormat)")
    }
}

How to draw images using Image views

var body: some View {
    Image("example-image")
}
Image(systemName: "cloud.heavyrain.fill")
    .foregroundColor(.red)
		.font(.largeTitle)

How to adjust the way an image is fitted to its space

Image("example-image")
    .resizable()
    .aspectRatio(contentMode: .fill)

How to render a gradient

Text("Hello World")
    .padding()
    .foregroundColor(.white)
    .background(LinearGradient(gradient: Gradient(colors: [.white, .black]), startPoint: .top, endPoint: .bottom), cornerRadius: 0)

支持更多颜色的渐变

Text("Hello World")
    .padding()
    .foregroundColor(.white)
    .background(LinearGradient(gradient: Gradient(colors: [.white, .red, .black]), startPoint: .leading, endPoint: .trailing), cornerRadius: 0)

How to display solid shapes

Rectangle()
    .fill(Color.red)
    .frame(width: 200, height: 200)

How to use images and other views as a backgrounds

Text("Hacking with Swift")
    .font(.largeTitle)
    .background(
        Image("example-image")
            .resizable()
            .frame(width: 100, height: 100))

View layout

Position views in a grid structure and more

How to create stacks using VStack and HStack

VStack {
    Text("SwiftUI")
    Text("rocks")
}

How to customize stack layouts with alignment and spacing

VStack(alignment: .leading, spacing: 20) {
    Text("SwiftUI")
    Text("rocks")
}

How to control spacing around individual views using padding

Text("SwiftUI")
    .padding(.bottom, 100)

How to layer views on top of each other using ZStack

ZStack {
           Rectangle()
               .fill(Color.red)
           Text("Hacking with Swift")
       }

How to return different view types

第一种方案

var body: some View {
    Group {
        if Bool.random() {
            Image("example-image")
        } else {
            Text("Better luck next time")
        }
    }
}

第二种方案

If you haven’t heard of this concept, it effectively forces Swift to forget about what specific type is inside the AnyView , allowing them to look like they are the same thing. This has a performance cost, though, so don’t use it often.

var body: AnyView {
    if Bool.random() {
        return AnyView(Image("example-image"))
    } else {
        return AnyView(Text("Better luck next time"))
    }
}

How to create views in a loop using ForEach

VStack(alignment: .leading) {
    ForEach((1...10).reversed()) {
        Text("($0)…")
    }

    Text("Ready or not, here I come!")
}
struct ContentView : View {
    let colors: [Color] = [.red, .green, .blue]

    var body: some View {
        VStack {
            ForEach(colors.identified(by: .self)) { color in
                Text(color.description.capitalized)
                    .padding()
                    .background(color)
            }
        }
    }
}

How to create different layouts using size classes

struct ContentView : View {
    @Environment(.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass?

    var body: some View {
        if horizontalSizeClass == .compact {
            return Text("Compact")
        } else {
            return Text("Regular")
        }
    }
}

How to place content outside the safe area

Text("Hello World")
    .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
    .background(Color.red)
    .edgesIgnoringSafeArea(.all)

Reading input

Respond to interaction and control your program state

Working with state

SwiftUI solves this problem by removing state from our control. When we add properties to our views they are effectively inert – they have values, sure, but changing them doesn’t do anything. But if we added the special @State attribute before them, SwiftUI will automatically watch for changes and update any parts of our views that use that state.

How to create a toggle switch

相反,我们应该定义一个 @State 布尔属性,用于存储切换的当前值。

struct ContentView : View {
    @State var showGreeting = true

    var body: some View {
        VStack {
            Toggle(isOn: $showGreeting) {
                Text("Show welcome message")
            }.padding()

            if showGreeting {
                Text("Hello World!")
            }
        }
    }
}

How to create a tappable button

struct ContentView : View {
    @State var showDetails = false

    var body: some View {
        VStack {
            Button(action: {
                self.showDetails.toggle()
            }) {
                Text("Show details")
            }

            if showDetails {
                Text("You should follow me on Twitter: @twostraws")
                    .font(.largeTitle)
                    .lineLimit(nil)
            }
        }
    }
}

How to read text from a TextField

struct ContentView : View {
    @State var name: String = "Tim"

    var body: some View {
        VStack {
            TextField($name)
            Text("Hello, (name)!")
        }
    }
}

How to add a border to a TextField

TextField($yourBindingHere)
    .textFieldStyle(.roundedBorder)

How to create secure text fields using SecureField

struct ContentView : View {
    @State var password: String = ""

    var body: some View {
        VStack {
            SecureField($password)
            Text("You entered: (password)")
        }
    }
}

How to create a Slider and read values from it

struct ContentView : View {
    @State var celsius: Double = 0

    var body: some View {
        VStack {
            Slider(value: $celsius, from: -100, through: 100, by: 0.1)
            Text("(celsius) Celsius is (celsius * 9 / 5 + 32) Fahrenheit")
        }
    }
}

How to create a date picker and read values from it

struct ContentView : View {
   var colors = ["Red", "Green", "Blue", "Tartan"]
   @State var selectedColor = 0

   var body: some View {
      VStack {
         Picker(selection: $selectedColor, label: Text("Please choose a color")) {
            ForEach(0 ..< colors.count) {
               Text(self.colors[$0]).tag($0)
            }
         }
         Text("You selected: (colors[selectedColor])")
      }
   }
}

How to create a segmented control and read values from it

struct ContentView : View {
    @State var favoriteColor = 0
    var colors = ["Red", "Green", "Blue"]

    var body: some View {
        VStack {
            SegmentedControl(selection: $favoriteColor) {
                ForEach(0..<colors.count) { index in
                    Text(self.colors[index]).tag(index)
                }
            }

            Text("Value: (colors[favoriteColor])")
        }
    }
}

How to read tap and double-tap gestures

Image("example-image")
    .tapAction(count: 2) {
        print("Double tapped!")
    }

How to add a gesture recognizer to a view

struct ContentView : View {
    @State var scale: Length = 1.0

    var body: some View {
        Image("example-image")
            .scaleEffect(scale)

            .gesture(
                TapGesture()
                    .onEnded { _ in
                        self.scale += 0.1
                    }
            )
    }
}

Lists

Create scrolling tables of data

Working with lists

SwiftUI’s List view is similar to UITableView in that it can show static or dynamic table view cells based on your needs.

How to create a list of static items

struct RestaurantRow: View {
    var name: String

    var body: some View {
        Text("Restaurant: (name)")
    }
}

struct ContentView: View {
    var body: some View {
        List {
            RestaurantRow(name: "Joe's Original")
            RestaurantRow(name: "The Real Joe's Original")
            RestaurantRow(name: "Original Joe's")
        }
    }
}

How to create a list of dynamic items

In order to handle dynamic items, you must first tell SwiftUI how it can identify which item is which. This is done using the Identifiable protocol, which has only one requirement: some sort of id value that SwiftUI can use to see which item is which.

ForEach 一样,可以使用符合 Identifiable 协议的 Model

struct Restaurant: Identifiable {
    var id = UUID()
    var name: String
}

struct RestaurantRow: View {
    var restaurant: Restaurant

    var body: some View {
        Text("Come and eat at (restaurant.name)")
    }
}

struct ContentView: View {
    var body: some View {
        let first = Restaurant(name: "Joe's Original")
        let second = Restaurant(name: "The Real Joe's Original")
        let third = Restaurant(name: "Original Joe's")
        let restaurants = [first, second, third]

        return List(restaurants) { restaurant in
            RestaurantRow(restaurant: restaurant)
        }
        // return List(restaurants, rowContent: RestaurantRow.init)
    }
}

How to add sections to a list

Section(header: Text("Other tasks"), footer: Text("End")) {
    TaskRow()
    TaskRow()
    TaskRow()
}

How to make a grouped list

struct ExampleRow: View {
    var body: some View {
        Text("Example Row")
    }
}

struct ContentView : View {
    var body: some View {
        List {
            Section(header: Text("Examples")) {
                ExampleRow()
                ExampleRow()
                ExampleRow()
            }
        }.listStyle(.grouped)
    }
}

Working with implicit stacking

What happens if you create a dynamic list and put more than one thing in each row? SwiftUI’s solution is simple, flexible, and gives us great behavior by default: it creates an implicit HStack to hold your items, so they automatically get laid out horizontally.

List 会隐式的创建一个 HStack 封装所有元素到 Row 中。

struct ExampleRow: View {
    var body: some View {
        Text("Example Row")
    }
}

struct ContentView : View {
    var body: some View {
        List {
            Section(header: Text("Examples")) {
                ExampleRow()
                ExampleRow()
                ExampleRow()
            }
        }.listStyle(.grouped)
    }
}

Containers

Place your views inside a navigation controller

Working with containers

SwiftUI is designed to be composed right out of the box, which means you can place one view inside another as much as you need.

常见的容器有: NavigationView , TabbedView , Group

NavigationView {
    Text("SwiftUI")
        .navigationBarTitle(Text("Welcome"))
}
var body: some View {
    NavigationView {
        Text("SwiftUI")
            .navigationBarTitle(Text("Welcome"))
            .navigationBarItems(trailing:
                Button(action: {
                    print("Help tapped!")
                }) {
                    Text("Help")
                })
    }
}

How to group views together

Stack 不能超过 10 个元素

var body: some View {
    VStack {
        Group {
            Text("Line")
            Text("Line")
            Text("Line")
            Text("Line")
            Text("Line")
            Text("Line")
        }

        Group {
            Text("Line")
            Text("Line")
            Text("Line")
            Text("Line")
            Text("Line")
        }
    }
}

Alerts and action sheets

Show modal notifications when something happens

Working with presentations

SwiftUI’s declarative approach to programming means that we don’t create and present alert and action sheets in the same way as we did in UIKit. Instead, we define the conditions in which they should be shown, tell it what they should look like, then leave it to figure the rest out for itself.

How to show an alert

struct ContentView : View {
    @State var showingAlert = false

    var body: some View {
        Button(action: {
            self.showingAlert = true
        }) {
            Text("Show Alert")
        }
        .presentation($showingAlert) {
            Alert(title: Text("Important message"), message: Text("Wear sunscreen"), dismissButton: .default(Text("Got it!")))
        }
    }
}

How to add actions to alert buttons

struct ContentView : View {
    @State var showingAlert = false

    var body: some View {
        Button(action: {
            self.showingAlert = true
        }) {
            Text("Show Alert")
        }
            .presentation($showingAlert) {
                Alert(title: Text("Are you sure you want to delete this?"), message: Text("There is no undo"), primaryButton: .destructive(Text("Delete")) {
                        print("Deleting...")
                }, secondaryButton: .cancel())
            }
    }
}

How to show an action sheet

struct ContentView : View {
    @State var showingSheet = false

    var sheet: ActionSheet {
        ActionSheet(title: Text("Action"), message: Text("Quote mark"), buttons: [.default(Text("Woo"), onTrigger: {
            self.showingSheet = false
        })])
    }

    var body: some View {
        Button(action: {
            self.showingSheet = true
        }) {
            Text("Woo")
        }
            .presentation(showingSheet ? sheet : nil)
    }
}

Presenting views

Move your user from one view to another

struct DetailView: View {
    var body: some View {
        Text("Detail")
    }
}

struct ContentView : View {
    var body: some View {
        NavigationView {
            NavigationButton(destination: DetailView()) {
                Text("Click")
            }.navigationBarTitle(Text("Navigation"))
        }
    }
}

How to push a new view when a list row is tapped

SwiftUI doesn’t have a direct equivalent of the didSelectRowAt method of UITableView , but it doesn’t need one because we can combine NavigationButton with a list row and get the behavior for free.

struct Restaurant: Identifiable {
    var id = UUID()
    var name: String
}

struct RestaurantRow: View {
    var restaurant: Restaurant

    var body: some View {
        Text(restaurant.name)
    }
}

struct RestaurantView: View {
    var restaurant: Restaurant

    var body: some View {
        Text("Come and eat at (restaurant.name)")
            .font(.largeTitle)
    }
}

struct ContentView: View {
    var body: some View {
        let first = Restaurant(name: "Joe's Original")
        let restaurants = [first]

        return NavigationView {
            List(restaurants) { restaurant in
                NavigationButton(destination: RestaurantView(restaurant: restaurant)) {
                    RestaurantRow(restaurant: restaurant)
                }
            }.navigationBarTitle(Text("Select a restaurant"))
        }
    }
}

How to present a new view using PresentationButton

struct DetailView: View {
    var body: some View {
        Text("Detail")
    }
}
struct ContentView : View {
    var body: some View {
        PresentationButton(Text("Click to show"), destination: DetailView())
    }
}

Transforming views

Clip, size, scale, spin, and more

How to give a view a custom frame

Button(action: {
    print("Button tapped")
}) {
    Text("Welcome")
        .frame(minWidth: 0, maxWidth: 200, minHeight: 0, maxHeight: 200)
        .font(.largeTitle)
}

Text("Please log in")
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
    .font(.largeTitle)
    .foregroundColor(.white)
    .background(Color.red)

How to adjust the position of a view

VStack {
    Text("Home")
    Text("Options")
        .offset(y: 15)
        .padding(.bottom, 15)
    Text("Help")
}

How to color the padding around a view

Padding 的先后顺序影响结果,因为代码是顺序执行的。

Text("Hacking with Swift")
    .background(Color.black)
    .foregroundColor(.white)
    .padding()

Text("Hacking with Swift")
    .padding()
    .background(Color.black)
    .foregroundColor(.white)

How to stack modifiers to create more advanced effects

Text("Forecast: Sun")
    .font(.largeTitle)
    .foregroundColor(.white)
    .padding()
    .background(Color.red)
    .padding()
    .background(Color.orange)
    .padding()
    .background(Color.yellow)

How to draw a border around a view

Text("Hacking with Swift")
    .padding()
    .border(Color.red, width: 4, cornerRadius: 16)

How to draw a shadow around a view

Text("Hacking with Swift")
    .padding()
    .border(Color.red, width: 4)
    .shadow(color: .red, radius: 5, x: 20, y: 20)

How to clip a view so only part is visible

Button(action: {
    print("Button tapped")
}) {
    Image(systemName: "bolt.fill")
        .foregroundColor(.white)
        .padding()
        .background(Color.green)
        .clipShape(Circle())
}

How to rotate a view

struct ContentView: View {
    @State var rotation: Double = 0

    var body: some View {
        VStack {
            Slider(value: $rotation, from: 0.0, through: 360.0, by: 1.0)
            Text("Up we go")
                .rotationEffect(.degrees(rotation))
        }
    }
}

How to rotate a view in 3D

SwiftUI’s rotation3DEffect() modifier lets us rotate views in 3D space to create beautiful effects in almost no code.

Text("EPISODE LLVM")
    .font(.largeTitle)
    .foregroundColor(.yellow)
    .rotation3DEffect(.degrees(45), axis: (x: 1, y: 0, z: 0))

How to scale a view up or down

Text("Up we go")
    .scaleEffect(5)

How to round the corners of a view

Text("Round Me")
    .padding()
    .background(Color.red)
    .cornerRadius(25)

How to adjust the opacity of a view

Text("Now you see me")
    .padding()
    .background(Color.red)
    .opacity(0.3)

How to adjust the accent color of a view

iOS uses tint colors to give apps a coordinated theme, and the same functionality is available in SwiftUI under the name accent colors .

How to mask one view with another

Image("stripes")
    .resizable()
    .frame(width: 300, height: 300)
    .mask(Text("SWIFT!")
        .font(Font.system(size: 72).weight(.black)))

How to blur a view

Text("Welcome to my SwiftUI app")
    .blur(radius: 2)

How to blend views together

ZStack {
    Image("paul-hudson")
    Image("example-image")
        .blendMode(.multiply)
}

How to adjust views by tinting, and desaturating, and more

Image("paul-hudson")
    .contrast(0.5)

Animation

Bring your views to life with movement

How to create a basic animation

struct ContentView: View {
    @State var angle: Double = 0
    @State var borderThickness: Length = 1

    var body: some View {
        Button(action: {
            self.angle += 45
            self.borderThickness += 1
        }) {
            Text("Tap here")
                .padding()
                .border(Color.red, width: borderThickness)
                .rotationEffect(.degrees(angle))
                .animation(.basic())
        }
    }
}

How to create a spring animation

struct ContentView: View {
    @State var angle: Double = 0

    var body: some View {
        Button(action: {
            self.angle += 45
        }) {
            Text("Tap here")
                .padding()
                .rotationEffect(.degrees(angle))
                .animation(.spring())
        }
    }
}

How to create an explicit animation

Explicit animations are often helpful because they cause every affected view to animation, not just those that have implicit animations attached. For example, if view A has to make room for view B as part of the animation, but only view B has an animation attached, then view A will jump to its new position without animating unless you use explicit animations.

struct ContentView: View {
    @State var opacity: Double = 1

    var body: some View {
        Button(action: {
            withAnimation {
                self.opacity -= 0.2
            }
        }) {
            Text("Tap here")
                .padding()
                .opacity(opacity)
        }
    }
}

How to add and remove views with a transition

struct ContentView: View {
    @State var showDetails = false

    var body: some View {
        VStack {
            Button(action: {
                withAnimation {
                    self.showDetails.toggle()
                }
            }) {
                Text("Tap to show details")
            }

            if showDetails {
                Text("Details go here.")
            }
        }
    }
}

By default, SwiftUI uses a fade animation to insert or remove views, but you can change that if you want by attaching a transition() modifier to a view.

Text("Details go here.")
    .transition(.move(edge: .bottom))

How to combine transitions

Text("Details go here.").transition(AnyTransition.opacity.combined(with: .slide))

或者使用拓展来封装常用的过渡效果

extension AnyTransition {
    static var moveAndScale: AnyTransition {
        AnyTransition.move(edge: .bottom).combined(with: .scale())
    }
}
// Usage
Text("Details go here.").transition(.moveAndScale)

How to create asymmetric transitions

SwiftUI lets us specify one transition when adding a view and another when removing it, all done using the asymmetric() transition type.

Text("Details go here.").transition(.asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .bottom)))

Composing views

Make your UI structure easier to understand

How to create and compose custom views

都是基于数据驱动 UI,所以每一级封装都是传 Model: User

struct User {
    var name: String
    var jobTitle: String
    var emailAddress: String
    var profilePicture: String
}

struct ProfilePicture: View {
    var imageName: String

    var body: some View {
        Image(imageName)
            .resizable()
            .frame(width: 100, height: 100)
            .clipShape(Circle())
    }
}

struct EmailAddress: View {
    var address: String

    var body: some View {
        HStack {
            Image(systemName: "envelope")
            Text(address)
        }
    }
}

struct UserDetails: View {
    var user: User

    var body: some View {
        VStack(alignment: .leading) {
            Text(user.name)
                .font(.largeTitle)
                .foregroundColor(.primary)
            Text(user.jobTitle)
                .foregroundColor(.secondary)
            EmailAddress(address: user.emailAddress)
        }
    }
}

struct UserView: View {
    var user: User

    var body: some View {
        HStack {
            ProfilePicture(imageName: user.profilePicture)
            UserDetails(user: user)
        }
    }
}

struct ContentView: View {
    let user = User(name: "Paul Hudson", jobTitle: "Editor, Hacking with Swift", emailAddress: "paul@hackingwithswift.com", profilePicture: "paul-hudson")

    var body: some View {
        UserView(user: user)
    }
}

How to combine text views together

这样子拼接文本很方便

var body: some View {
    Text("SwiftUI")
        .font(.largeTitle)
    + Text("is")
        .font(.headline)
    + Text("awesome")
        .font(.footnote)
}

但是有个修改器不适合拼接如: foregroundColor

这时候就需要换成能够使用 + 的修改器,如 color

Text("SwiftUI")
    .color(.red)
+ Text("is")
    .color(.orange)
+ Text("awesome")
    .color(.blue)

How to store views as properties

struct ContentView : View {
    let title = Text("Paul Hudson")
        .font(.largeTitle)
    let subtitle = Text("Author")
        .foregroundColor(.secondary)
    
    var body: some View {
        VStack {
            title
            .color(.red)
            subtitle
        }
    }
}

How to create custom modifiers

struct PrimaryLabel: ViewModifier {
    func body(content: Content) -> some View {
        content
            .padding()
            .background(Color.red)
            .foregroundColor(Color.white)
            .font(.largeTitle)
    }
}

struct ContentView: View {
    var body: some View {
        Text("Hello, SwiftUI")
            .modifier(PrimaryLabel())
    }
}

Tooling

Build better apps with help from Xcode

How to preview your layout at different Dynamic Type sizes

#if DEBUG
struct ContentView_Previews : PreviewProvider {
   static var previews: some View {
      Group {
         ContentView()
            .environment(.colorScheme, .dark)
      }
   }
}
#endif

How to preview your layout in light and dark mode

If you want to see both light and dark mode side by side, place multiple previews in a group, like this:

#if DEBUG
struct ContentView_Previews : PreviewProvider {
   static var previews: some View {
      Group {
         ContentView()
            .environment(.colorScheme, .light)

         ContentView()
            .environment(.colorScheme, .dark)
      }
   }
}
#endif

How to preview your layout in different devices

ContentView()
    .previewDevice(PreviewDevice(rawValue: "iPhone SE"))
struct ContentView : View {
   var body: some View {
      Text("Hello World")
         .navigationBarTitle(Text("Welcome"))
   }
}

#if DEBUG
struct ContentView_Previews : PreviewProvider {
   static var previews: some View {
      NavigationView {
         ContentView()
      }
   }
}
#endif

How to use Instruments to profile your SwiftUI code and identify slow layouts

示例代码

import Combine
import SwiftUI

class FrequentUpdater: BindableObject {
   var didChange = PassthroughSubject<Void, Never>()
   var timer: Timer?

   init() {
      timer = Timer.scheduledTimer(
         withTimeInterval: 0.01,
         repeats: true
      ) { _ in
         self.didChange.send(())
      }
   }
}

struct ContentView : View {
   @ObjectBinding var updater = FrequentUpdater()
   @State var tapCount = 0

   var body: some View {
      VStack {
         Text("(UUID().uuidString)")

         Button(action: {
            self.tapCount += 1
         }) {
            Text("Tap count: (tapCount)")
         }
      }
   }
}

检测我们的代码

默认情况下,SwiftUI 工具告诉我们各种各样的事情:

1. 在此期间创建了多少视图以及创建它们需要多长时间(“View Body”)

2. 视图的属性是什么以及它们如何随时间变化(“View Properties”)

3. 发生了多少次 Core Animation 提交(“Core Animation Commits”)

4. 每个函数调用的确切时间(“Time Profiler”)

监视 body 调用

如果您选择 View Body 轨道 – 这是 instrument 列表中的第一行 – 您应该能够看到乐器将结果分解为 SwiftUI 和您的项目,前者是原始类型,如文本视图和按钮,以及后者包含您的自定义视图类型。在我们的例子中,这意味着“ContentView”应该出现在自定义视图中,因为这是我们视图的名称。

现在,您在这里看不到的是您的代码与 SwiftUI 视图的完美一对一映射,因为 SwiftUI 积极地折叠其视图层次结构以尽可能少地完成工作。所以,不要指望在代码中看到任何 VStack 创建 – 这个应用程序实际上是免费的。

在这个屏幕上,重要的数字是计数和平均持续时间 – 每件事创建的次数,以及花费的时间。因为这是一个压力测试你应该看到非常高的数字,但我们的布局是微不足道的,所以平均持续时间可能是几十微秒。

跟踪状态(state)变化

接下来,选择“View Properties”轨道,这是仪器列表中的第二行。这将显示所有视图的所有属性,包括其当前值和所有先前值。

我们的示例应用程序有一个按钮,通过在数字中添加一个来更改其标签,并且在此工具中可见 – 请查看视图类型 ContentView 和属性类型 State

可悲的是,Instruments 还没有(还能)向我们展示那里的确切属性名称,如果你跟踪了几个整数状态,这可能会更加令人困惑。然而,它确实有一个不同的技巧:在记录窗口的顶部是一个标记当前视图位置的箭头,如果你拖动它,你会看到应用程序状态随时间的变化 – 每次你点击按钮,你会看到状态整数上升一个,你可以前进和后退来看它发生。

这可以释放巨大的能力,因为它可以让我们直接看到状态变化导致慢速重绘或其他工作 – 这几乎就像是在时间机器中,您可以在运行期间的每个点检查应用程序的确切状态。

识别慢速绘图

虽然 SwiftUI 能够直接调用 Metal 以提高性能,但大多数情况下它更喜欢使用 Core Animation 进行渲染。这意味着我们会自动从 Instruments 获取内置的 Core Animation 分析工具,包括检测昂贵提交(expensive commits)的能力。

当多个更改放在一个组中时,Core Animation 的效果最佳,称为 transaction 。我们在一个事务中有效地堆叠了一系列工作,然后要求 CA 继续渲染工作 – 称为 提交 事务。

因此,当 Instruments 向我们展示昂贵的 Core Animation 提交时,它真正向我们展示的是 SwiftUI 因为更新而被迫重绘屏幕上的像素的次数。理论上,这应该只在我们的应用程序的实际状态导致不同的视图层次结构时发生,因为 SwiftUI 应该能够将我们的 body 属性的新输出与先前的输出进行比较。

寻找缓慢的函数调用

Time Profiler,它向我们展示了在代码的每个部分花费了多少时间。这与乐器中的常规时间分析器完全相同,但如果您之前没有尝试过,那么您至少需要知道:

  1. 右侧的扩展详细信息窗口默认显示最重的堆栈跟踪,这是运行时间最长的代码段。明亮的代码(白色或黑色,取决于您的 macOS 配色方案)是您编写的代码; 昏暗代码(灰色)是系统库代码。

  2. 在左侧,您可以看到创建的所有线程,以及公开指示器,让您深入了解它们调用的函数以及这些函数调用的函数等。大多数工作将在“start”内部进行。

  3. 为避免混乱,您可能需要单击底部的“调用树”按钮,然后选择“隐藏系统库”。这只会显示您编写的代码,但是如果您的问题是您使用的系统库很糟糕,这可能没有帮助。

  4. 要直接了解具体细节,您还可以单击“调用树”并选择“反转调用树”以翻转事物,以便叶子功能(树末端的功能)显示在顶部,现在可以向下钻取公开指示器(向上钻取?)到调用它们的函数。

最后一些提示

在您收取配置自己的代码之前,有一些事情需要注意:

  1. 在检查应用程序性能的一小部分时,您应该单击并拖动相关范围,以便仅查看该应用程序部分的统计信息。这使您可以专注于特定操作的性能,例如响应按下按钮。
  2. 即使你在仪器中看到纯色条,它们只是从远处看起来那样 – 你可以通过按住 Cmd 并按 – 和 + 来查看更多细节
  3. 要获得最准确的数字,请始终在真实设备上进行配置。
  4. 如果要通过分析代码进行更改,请始终一次进行一次更改。如果你进行两次更改,可能会使你的性能提高 20%而另一种会降低 10%,但是将它们合在一起意味着你可能会认为它们整体性能提高了 10%。
  5. Instruments 在 release 模式下运行您的代码,从而实现 Swift 的所有优化。这也会影响您添加到代码中的任何调试标志,因此请小心。

What now?

How to continue learning SwiftUI after the basics

SwiftUI tips and tricks

SwiftUI 拥有强大的标题功能,但也有许多较小的提示和技巧可以帮助您编写更好的应用程序。

@State 设为私有

@State private var score = 0

具有常量绑定的原型

TextField(.constant("Hello"))
    .textFieldStyle(.roundedBorder)

使用语义颜色

Color.red

依靠自适应填充

Text("Row 1")
    .padding(10)

合并文本视图

struct ContentView : View {
    var body: some View {
        Text("Colored")
            .color(.red)
        +
        Text("SwifUI")
                .color(.green)
        +
        Text("Text")
            .color(.blue)
    }
}

如何使 print() 工作

右键单击预览画布(preview canvas)中的播放按钮,然后选择“调试预览(Debug Preview)”。

依靠隐式 HStack

struct ContentView : View {
    let imageNames = ["paul-hudson", "swiftui"]

    var body: some View {
        List(imageNames.identified(by: .self)) { image in
            Image(image).resizable().frame(width: 40)
            Text(image)
        }
    }
}

分割大视图

struct ContentView : View {
    let users = ["Paul Hudson", "Taylor Swift"]

    var body: some View {
        NavigationView {
            List(users.identified(by: .self)) { user in
                NavigationButton(destination: Text("Detail View")) {
                    Image("example-image").resizable().frame(width: 50, height: 50)

                    VStack(alignment: .leading) {
                        Text("Johnny Appleseed").font(.headline)
                        Text("Occupation: Programmer")
                    }
                }
            }.navigationBarTitle(Text("Users"))
        }
    }
}

更好的预览

#if DEBUG
struct ContentView_Previews : PreviewProvider {
    static var previews: some View {
        Group {
            ContentView()
                .environment(.sizeCategory, .accessibilityExtraExtraExtraLarge)
            ContentView()
                .environment(.colorScheme, .dark)
            NavigationView {
                ContentView()
            }
        }
    }
}
#endif

创建自定义修改器

struct PrimaryLabel: ViewModifier {
    func body(content: Content) -> some View {
        content
            .padding()
            .background(Color.black)
            .foregroundColor(Color.white)
            .font(.largeTitle)
            .cornerRadius(10)
    }
}

struct ContentView : View {
    var body: some View {
        Text("Hello World")
            .modifier(PrimaryLabel())
    }
}

动画变化很容易

struct ContentView : View {
    @State var showingWelcome = false

    var body: some View {
        VStack {
            Toggle(isOn: $showingWelcome.animation(.spring())) {
                Text("Toggle label")
            }

            if showingWelcome {
                Text("Hello World")
            }
        }
    }
}

阅读原文...


Avatar

Blurring a div blurs other divs

上一篇

On the Road: Russia, UK and France Summer MeetUps

下一篇

您也可能喜欢

评论已经被关闭。

插入图片
SwiftUI By Example 笔记

长按储存图像,分享给朋友