原文为 @twostraws 的 SwiftUI By Example ,就是简单记录一些主要内容。
Introduction A brief explanation of the basics of SwiftUI
What is SwiftUI?
声明式对比命令式,能够更好地处理不同状态的UI
跨平台:支持 iOS, macOS, tvOS, watchOS
SwiftUI vs Interface Builder and storyboards
IB难以阅读和修改
IB难以看出修改了内容
IB和Swift交互不够友好,充满了Objective-C设计
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
SceneDelegate.swift is responsible for managing the way your app is shown.
1 2 3 4 let window = UIWindow (frame: UIScreen .main.bounds)window.rootViewController = UIHostingController (rootView: ContentView ()) self .window = windowwindow.makeKeyAndVisible()
Open ContentView.swift and let’s look at some actual SwiftUI code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import SwiftUIstruct 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 view
。 some
关键字是Swift 5.1中的新关键字,是名为opaque return types 的功能的一部分,在这种情况下,就是字面意思:”这将返回某种View
,但SwiftUI不需要知道(或关心)什么。”
Finally, below ContentView
is a similar-but-different struct called ContentView_Previews
.
1 2 Text ("Hello World" ) .lineLimit(3 )
1 2 Text ("This is an extremely long string that will never fit even the widest of Phones" ) .truncationMode(.middle)
1 2 3 4 5 6 7 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 )
1 2 3 4 5 6 7 8 9 10 11 12 13 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) " ) } }
1 2 3 var body: some View { Image ("example-image" ) }
1 2 3 Image (systemName: "cloud.heavyrain.fill" ) .foregroundColor(.red) .font(.largeTitle)
1 2 3 Image ("example-image" ) .resizable() .aspectRatio(contentMode: .fill)
1 2 3 4 Text ("Hello World" ) .padding() .foregroundColor(.white) .background(LinearGradient (gradient: Gradient (colors: [.white, .black]), startPoint: .top, endPoint: .bottom), cornerRadius: 0 )
支持更多颜色的渐变
1 2 3 4 Text ("Hello World" ) .padding() .foregroundColor(.white) .background(LinearGradient (gradient: Gradient (colors: [.white, .red, .black]), startPoint: .leading, endPoint: .trailing), cornerRadius: 0 )
1 2 3 Rectangle () .fill(Color .red) .frame(width: 200 , height: 200 )
1 2 3 4 5 6 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
1 2 3 4 VStack { Text ("SwiftUI" ) Text ("rocks" ) }
1 2 3 4 VStack (alignment: .leading, spacing: 20 ) { Text ("SwiftUI" ) Text ("rocks" ) }
1 2 Text ("SwiftUI" ) .padding(.bottom, 100 )
1 2 3 4 5 ZStack { Rectangle () .fill(Color .red) Text ("Hacking with Swift" ) }
第一种方案
1 2 3 4 5 6 7 8 9 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.
1 2 3 4 5 6 7 var body: AnyView { if Bool .random() { return AnyView (Image ("example-image" )) } else { return AnyView (Text ("Better luck next time" )) } }
1 2 3 4 5 6 7 VStack (alignment: .leading) { ForEach ((1 ... 10 ).reversed()) { Text ("\($0 ) …" ) } Text ("Ready or not, here I come!" ) }
1 2 3 4 5 6 7 8 9 10 11 12 13 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) } } } }
1 2 3 4 5 6 7 8 9 10 11 struct ContentView : View { @Environment (\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass ? var body: some View { if horizontalSizeClass == .compact { return Text ("Compact" ) } else { return Text ("Regular" ) } } }
1 2 3 4 Text ("Hello World" ) .frame(minWidth: 0 , maxWidth: .infinity, minHeight: 0 , maxHeight: .infinity) .background(Color .red) .edgesIgnoringSafeArea(.all)
Respond to interaction and control your program 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.
相反,我们应该定义一个@State
布尔属性,用于存储切换的当前值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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!" ) } } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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 ) } } } }
1 2 3 4 5 6 7 8 9 10 struct ContentView : View { @State var name: String = "Tim" var body: some View { VStack { TextField ($name ) Text ("Hello, \(name) !" ) } } }
1 2 TextField ($yourBindingHere ) .textFieldStyle(.roundedBorder)
1 2 3 4 5 6 7 8 9 10 struct ContentView : View { @State var password: String = "" var body: some View { VStack { SecureField ($password ) Text ("You entered: \(password) " ) } } }
1 2 3 4 5 6 7 8 9 10 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" ) } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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]) " ) } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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]) " ) } } }
1 2 3 4 Image ("example-image" ) .tapAction(count: 2 ) { print ("Double tapped!" ) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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
SwiftUI’s List
view is similar to UITableView
in that it can show static or dynamic table view cells based on your needs.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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" ) } } }
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
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 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) } } }
1 2 3 4 5 Section (header: Text ("Other tasks" ), footer: Text ("End" )) { TaskRow () TaskRow () TaskRow () }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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) } }
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中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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
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
1 2 3 4 NavigationView { Text ("SwiftUI" ) .navigationBarTitle(Text ("Welcome" )) }
1 2 3 4 5 6 7 8 9 10 11 12 var body: some View { NavigationView { Text ("SwiftUI" ) .navigationBarTitle(Text ("Welcome" )) .navigationBarItems(trailing: Button (action: { print ("Help tapped!" ) }) { Text ("Help" ) }) } }
Stack
不能超过10个元素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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!" ))) } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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()) } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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" )) } } }
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.
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 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" )) } } }
1 2 3 4 5 6 7 8 9 10 struct DetailView : View { var body: some View { Text ("Detail" ) } } struct ContentView : View { var body: some View { PresentationButton (Text ("Click to show" ), destination: DetailView ()) } }
Clip, size, scale, spin, and more
1 2 3 4 5 6 7 8 9 10 11 12 13 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)
1 2 3 4 5 6 7 VStack { Text ("Home" ) Text ("Options" ) .offset(y: 15 ) .padding(.bottom, 15 ) Text ("Help" ) }
Padding 的先后顺序影响结果,因为代码是顺序执行的。
1 2 3 4 5 6 7 8 9 Text ("Hacking with Swift" ) .background(Color .black) .foregroundColor(.white) .padding() Text ("Hacking with Swift" ) .padding() .background(Color .black) .foregroundColor(.white)
1 2 3 4 5 6 7 8 9 Text ("Forecast: Sun" ) .font(.largeTitle) .foregroundColor(.white) .padding() .background(Color .red) .padding() .background(Color .orange) .padding() .background(Color .yellow)
1 2 3 Text ("Hacking with Swift" ) .padding() .border(Color .red, width: 4 , cornerRadius: 16 )
1 2 3 4 Text ("Hacking with Swift" ) .padding() .border(Color .red, width: 4 ) .shadow(color: .red, radius: 5 , x: 20 , y: 20 )
1 2 3 4 5 6 7 8 9 Button (action: { print ("Button tapped" ) }) { Image (systemName: "bolt.fill" ) .foregroundColor(.white) .padding() .background(Color .green) .clipShape(Circle ()) }
1 2 3 4 5 6 7 8 9 10 11 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)) } } }
SwiftUI’s rotation3DEffect()
modifier lets us rotate views in 3D space to create beautiful effects in almost no code.
1 2 3 4 Text ("EPISODE LLVM" ) .font(.largeTitle) .foregroundColor(.yellow) .rotation3DEffect(.degrees(45 ), axis: (x: 1 , y: 0 , z: 0 ))
1 2 Text ("Up we go" ) .scaleEffect(5 )
1 2 3 4 Text ("Round Me" ) .padding() .background(Color .red) .cornerRadius(25 )
1 2 3 4 Text ("Now you see me" ) .padding() .background(Color .red) .opacity(0.3 )
iOS uses tint colors to give apps a coordinated theme, and the same functionality is available in SwiftUI under the name accent colors .
1 2 3 4 5 Image ("stripes" ) .resizable() .frame(width: 300 , height: 300 ) .mask(Text ("SWIFT!" ) .font(Font .system(size: 72 ).weight(.black)))
1 2 Text ("Welcome to my SwiftUI app" ) .blur(radius: 2 )
1 2 3 4 5 ZStack { Image ("paul-hudson" ) Image ("example-image" ) .blendMode(.multiply) }
1 2 Image ("paul-hudson" ) .contrast(0.5 )
Animation Bring your views to life with movement
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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()) } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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()) } } }
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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) } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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.
1 2 Text ("Details go here." ) .transition(.move(edge: .bottom))
1 Text ("Details go here." ).transition(AnyTransition .opacity.combined(with: .slide))
或者使用拓展来封装常用的过渡效果
1 2 3 4 5 6 7 extension AnyTransition { static var moveAndScale: AnyTransition { AnyTransition .move(edge: .bottom).combined(with: .scale()) } } Text ("Details go here." ).transition(.moveAndScale)
SwiftUI lets us specify one transition when adding a view and another when removing it, all done using the asymmetric()
transition type.
1 Text ("Details go here." ).transition(.asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .bottom)))
Composing views Make your UI structure easier to understand
都是基于数据驱动UI,所以每一级封装都是传Model:User
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 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) } }
这样子拼接文本很方便
1 2 3 4 5 6 7 8 var body: some View { Text ("SwiftUI " ) .font(.largeTitle) + Text ("is " ) .font(.headline) + Text ("awesome" ) .font(.footnote) }
但是有个修改器不适合拼接如:foregroundColor
这时候就需要换成能够使用+
的修改器,如color
1 2 3 4 5 6 Text ("SwiftUI " ) .color(.red) + Text ("is " ) .color(.orange) + Text ("awesome" ) .color(.blue)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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 } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 ()) } }
Build better apps with help from Xcode
1 2 3 4 5 6 7 8 9 10 #if DEBUG struct ContentView_Previews : PreviewProvider { static var previews: some View { Group { ContentView () .environment(\.colorScheme, .dark) } } } #endif
If you want to see both light and dark mode side by side, place multiple previews in a group, like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 #if DEBUG struct ContentView_Previews : PreviewProvider { static var previews: some View { Group { ContentView () .environment(\.colorScheme, .light) ContentView () .environment(\.colorScheme, .dark) } } } #endif
1 2 ContentView () .previewDevice(PreviewDevice (rawValue: "iPhone SE" ))
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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
示例代码
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 import Combineimport SwiftUIclass 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,它向我们展示了在代码的每个部分花费了多少时间。这与乐器中的常规时间分析器完全相同,但如果您之前没有尝试过,那么您至少需要知道:
右侧的扩展详细信息窗口默认显示最重的堆栈跟踪,这是运行时间最长的代码段。明亮的代码(白色或黑色,取决于您的macOS配色方案)是您编写的代码;昏暗代码(灰色)是系统库代码。
在左侧,您可以看到创建的所有线程,以及公开指示器,让您深入了解它们调用的函数以及这些函数调用的函数等。大多数工作将在“start”内部进行。
为避免混乱,您可能需要单击底部的“调用树”按钮,然后选择“隐藏系统库”。这只会显示您编写的代码,但是如果您的问题是您使用的系统库很糟糕,这可能没有帮助。
要直接了解具体细节,您还可以单击“调用树”并选择“反转调用树”以翻转事物,以便叶子功能(树末端的功能)显示在顶部,现在可以向下钻取公开指示器(向上钻取?)到调用它们的函数。
最后一些提示 在您收取配置自己的代码之前,有一些事情需要注意:
在检查应用程序性能的一小部分时,您应该单击并拖动相关范围,以便仅查看该应用程序部分的统计信息。这使您可以专注于特定操作的性能,例如响应按下按钮。
即使你在仪器中看到纯色条,它们只是从远处看起来那样 - 你可以通过按住Cmd并按 - 和+来查看更多细节
要获得最准确的数字,请始终在真实设备上进行配置。
如果要通过分析代码进行更改,请始终一次进行一次更改。如果你进行两次更改,可能会使你的性能提高20%而另一种会降低10%,但是将它们合在一起意味着你可能会认为它们整体性能提高了10%。
Instruments在release模式下运行您的代码,从而实现Swift的所有优化。这也会影响您添加到代码中的任何调试标志,因此请小心。
What now? How to continue learning SwiftUI after the basics
SwiftUI拥有强大的标题功能,但也有许多较小的提示和技巧可以帮助您编写更好的应用程序。
@State 设为私有 1 @State private var score = 0
具有常量绑定的原型 1 2 TextField (.constant("Hello" )) .textFieldStyle(.roundedBorder)
使用语义颜色
依靠自适应填充 1 2 Text ("Row 1" ) .padding(10 )
合并文本视图 1 2 3 4 5 6 7 8 9 10 11 12 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
1 2 3 4 5 6 7 8 9 10 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) } } }
分割大视图 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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" )) } } }
更好的预览 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #if DEBUG struct ContentView_Previews : PreviewProvider { static var previews: some View { Group { ContentView () .environment(\.sizeCategory, .accessibilityExtraExtraExtraLarge) ContentView () .environment(\.colorScheme, .dark) NavigationView { ContentView () } } } } #endif
创建自定义修改器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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 ()) } }
动画变化很容易 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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" ) } } } }