SwiftUI小项目
Project1WeSplit
- WeSplit: Introduction
- Understanding the basic structure of a SwiftUI app
- Creating a form
- Adding a navigation bar
- Modifying program state
- Binding state to user interface controls
- Creating views in a loop
- Reading text from the user with TextField
- Creating pickers in a form
- Adding a segmented control for tip percentages
- Calculating the total per person
- Hiding the keyboard
为什么navigationTitle是放在NavigationView里面而不是外面?
It’s tempting to think that modifier should be attached to the end of the
NavigationView
, but it needs to be attached to the end of theForm
instead. The reason is that navigation views are capable of showing many views as your program runs, so by attaching the title to the thing inside the navigation view we’re allowing iOS to change titles freely.
.alert()放在哪里貌似也不打紧
That attaches the alert to the button, but honestly it doesn’t matter where the alert()
modifier is used – all we’re doing is saying that an alert exists and is shown when showingAlert
is true.
一些状态注解, Property wrappers
Property wrappers have that name because they wrap our property inside another struct. What this means is that when we use @State
to wrap a string, the actual type of property we end up with is a State<String>
. Similarly, when we use @Environment
and others we end up with a struct of type Environment
that contains some other value inside it.
1 | // cmd + shift + O |
How property wrappers become structs
就是说,Slider这种two-way绑定的,用didSet是看不到变化的,因为property wrapper是将比如String封装在一个新的struct中,然后slider变了的话,他是直接传到绑定的UI里面去了。用onChange() modifier可以代替didSet。(Project13)
That wrapped value is the actual value we’re trying to store, such as a string. What this generated interface is telling us is that the property can be read (get), and written (set), but that when we set the value it won’t actually change the struct itself. Behind the scenes, it sends that value off to SwiftUI for storage in a place where it can be modified freely, so it’s true that the struct itself never changes.
@State
配合$,可以组成two-way binding,designed for simple data that is local to the current view@Binding
,lets us connect an@State
property of one view to some underlying model data.Published(objectWillChange.send()两者选一个,这个可以添加一些额外的操作)(属性)
,@StateObject(创建)
,@ObservedObject(取出)
,@ObservableObject(协议),放入是当作构造参数放入的。User
is a class the property itself isn’t changing, so@State
doesn’t notice anything and can’t reload the view. Yes, the values inside the class are changing, but@State
doesn’t monitor those, so effectively what’s happening is that the values inside our class are being changed but the view isn’t being reloaded to reflect that change.
用objectWillChange.send()代替@Published。
1 | var value = 0 { |
@EnvironmentObject(取出)
, @StateObject(创建), @ObservableObject(协议), @Published(属性), .environmentObject()(放入)@Environment
1 | var dismiss (\.dismiss) |
@FocusState
区别
@State
is used to work with state that is local to a single view, and how
@ObservedObject
lets us pass one object from view to view so we can share it. Well,
@EnvironmentObject
takes that one step further: we can place an object into the environment so that any child view can automatically have access to it.
Imagine we had multiple views in an app, all lined up in a chain: view A shows view B, view B shows view C, C shows D, and D shows E. View A and E both want to access the same object, but to get from A to E you need to go through B, C, and D, and they don’t care about that object. If we were using @ObservedObject
we’d need to pass our object from each view to the next until it finally reached view E where it could be used, which is annoying because B, C, and D don’t care about it. With @EnvironmentObject
view A can put the object into the environment, view E can read the object out from the environment, and views B, C, and D don’t have to know anything happened – it’s much nicer.
There’s one last thing before I show you some code: environment objects use the same ObservableObject
protocol you’ve already learned, and SwiftUI will automatically make sure all views that share the same environment object get updated when it changes.
1 | class User: ObservableObject { |
how SwiftUI makes the connection between .environmentObject(user)
and @EnvironmentObject var user: User
– how does it know to place that object into the correct property?
Well, you’ve seen how dictionaries let us use one type for the key and another for the value. The environment effectively lets us use data types themselves for the key, and instances of the type as the value. This is a bit mind bending at first, but imagine it like this: the keys are things like Int
, String
, and Bool
, with the values being things like 5, “Hello”, and true, which means we can say “give me the Int
” and we’d get back 5.
Project2 GuesstheFlag
为什么视图在swiftUI中是结构体而不是类
If you ever programmed for UIKit or AppKit (Apple’s original user interface frameworks for iOS and macOS) you’ll know that they use classes for views rather than structs. SwiftUI does not: we prefer to use structs for views across the board, and there are a couple of reasons why.
First, there is an element of performance: structs are simpler and faster than classes. I say an element of performance because lots of people think this is the primary reason SwiftUI uses structs, when really it’s just one part of the bigger picture.
In UIKit, every view descended from a class called
UIView
that had many properties and methods – a background color, constraints that determined how it was positioned, a layer for rendering its contents into, and more. There were lots of these, and everyUIView
andUIView
subclass had to have them, because that’s how inheritance works.In SwiftUI, all our views are trivial structs and are almost free to create. Think about it: if you make a struct that holds a single integer, the entire size of your struct is… that one integer. Nothing else. No surprise extra values inherited from parent classes, or grandparent classes, or great-grandparent classes, etc – they contain exactly what you can see and nothing more.
Thanks to the power of modern iPhones, I wouldn’t think twice about creating 1000 integers or even 100,000 integers – it would happen in the blink of an eye. The same is true of 1000 SwiftUI views or even 100,000 SwiftUI views; they are so fast it stops being worth thinking about.
However, even though performance is important there’s something much more important about views as structs: it forces us to think about isolating state in a clean way. You see, classes are able to change their values freely, which can lead to messier code – how would SwiftUI be able to know when a value changed in order to update the UI?
By producing views that don’t mutate over time, SwiftUI encourages us to move to a more functional design approach: our views become simple, inert things that convert data into UI, rather than intelligent things that can grow out of control.
You can see this in action when you look at the kinds of things that can be a view. We already used
Color.red
andLinearGradient
as views – trivial types that hold very little data. In fact, you can’t get a great deal simpler than usingColor.red
as a view: it holds no information other than “fill my space with red”.In comparison, Apple’s documentation for UIView lists about 200 properties and methods that
UIView
has, all of which get passed on to its subclasses whether they need them or not.
Project3 Views and Modifiers
- Views and modifiers: Introduction
- Why does SwiftUI use structs for views?
- What is behind the main SwiftUI view?
- Why modifier order matters
- Why does SwiftUI use “some View” for its view type?
- Conditional modifiers
- Environment modifiers
- Views as properties
- View composition
- Custom modifiers
Views and Modifiers的结构
Whenever we apply a modifier to a SwiftUI view, we actually create a new view with that change applied – we don’t just modify the existing view in place. If you think about it, this behavior makes sense: our views only hold the exact properties we give them, so if we set the background color or font size there is no place to store that data.
We’re going to look at why this happens shortly, but first I want to look at the practical implications of this behavior. Take a look at this code:
1
2
3
4
5 Button("Hello, world!") {
// do nothing
}
.background(.red)
.frame(width: 200, height: 200)What do you think that will look like when it runs?
Chances are you guessed wrong: you won’t see a 200x200 red button with “Hello, world!” in the middle. Instead, you’ll see a 200x200 empty square, with “Hello, world!” in the middle and with a red rectangle directly around “Hello, world!”.
You can understand what’s happening here if you think about the way modifiers work: each one creates a new struct with that modifier applied, rather than just setting a property on the view.
Of course, this isn’t actually how SwiftUI works, because if it did it would be a performance nightmare, but it’s a neat mental shortcut to use while you’re learning.
An important side effect of using modifiers is that we can apply the same effect multiple times: each one simply adds to whatever was there before.
Environment modifiers
Many modifiers can be applied to containers, which allows us to apply the same modifier to many views at the same time.
For example, if we have four text views in a
VStack
and want to give them all the same font modifier, we could apply the modifier to theVStack
directly and have that change apply to all four text views:
1
2
3
4
5
6
7 VStack {
Text("Gryffindor")
Text("Hufflepuff")
Text("Ravenclaw")
Text("Slytherin")
}
.font(.title)This is called an environment modifier, and is different from a regular modifier that is applied to a view.
From a coding perspective these modifiers are used exactly the same way as regular modifiers. However, they behave subtly differently because if any of those child views override the same modifier, the child’s version takes priority.
1
2
3
4
5
6
7
8 VStack {
Text("Gryffindor")
.font(.largeTitle)
Text("Hufflepuff")
Text("Ravenclaw")
Text("Slytherin")
}
.font(.title)
font()
is an environment modifier, which means the Gryffindor text view can override it with a custom font.
1
2
3
4
5
6
7
8 VStack {
Text("Gryffindor")
.blur(radius: 0)
Text("Hufflepuff")
Text("Ravenclaw")
Text("Slytherin")
}
.blur(radius: 5)Above won’t work the same way:
blur()
is a regular modifier, so any blurs applied to child views are added to theVStack
blur rather than replacing it.
What happens if we send back two views directly from our body
property, without wrapping them in a stack?
Swift silently applies a special attribute to the
body
property called@ViewBuilder
. This has the effect of silently wrapping multiple views in one of thoseTupleView
containers, so that even though it looks like we’re sending back multiple views they get combined into oneTupleView
.
Custom modifiers
1 | struct Watermark: ViewModifier { |
1 | Text("Hello World") |
1 | extension View { |
1 | Color.blue |
Tip: Often folks wonder when it’s better to add a custom view modifier versus just adding a new method to
View
, and really it comes down to one main reason: custom view modifiers can have their own stored properties, whereas extensions toView
cannot.
Project4 BetterRest
- BetterRest: Introduction
- Entering numbers with Stepper
- Selecting dates and times with DatePicker
- Working with dates
- Training a model with Create ML
- Building a basic layout
- Connecting SwiftUI to Core ML
- Cleaning up the user interface
隐藏DatePicker的 label
1 | DatePicker("Please enter a date", selection: $wakeUp) |
虽然讲label隐藏,但是旁白还是能读取的。
That still includes the original label so screen readers can use it for VoiceOver, but now they aren’t visible onscreen any more – the date picker won’t be pushed to one side by some empty text.
关于Date的一些语法
Project5 WordScramble
- Word Scramble: Introduction
- Introducing List, your best friend
- Loading resources from your app bundle
- Working with strings
- Adding to a list of words
- Running code when our app launches
- Validating words with UITextChecker
Form和List区别
The job of
List
is to provide a scrolling table of data. In fact, it’s pretty much identical toForm
, except it’s used for presentation of data rather than requesting user input. Don’t get me wrong: you’ll useForm
quite a lot too, but really it’s just a specialized type ofList
.Now, everything you’ve seen so far works fine with
Form
as well asList
– even the dynamic content. But one thingList
can do thatForm
can’t is to generate its rows entirely from dynamic content without needing aForEach
.
Project6 Animations
- Animation: Introduction
- Creating implicit animations
- Customizing animations in SwiftUI
- Animating bindings
- Creating explicit animations
- Controlling the animation stack
- Animating gestures
- Showing and hiding views with transitions
- Building custom transitions using ViewModifier
Remember
Remember: the order in which you apply modifiers *matters*, and nowhere is this more true than when working with offsets and rotations.
If we rotate then offset, then the offset is applied based on the rotated axis of our view. For example, if we move something 100 pixels to its left then rotate 90 degrees, we’d end up with it being 100 pixels to the left and rotated 90 degrees. But if we rotated 90 degrees then moved it 100 pixels to its left, we’d end up with something rotated 90 degrees and moved 100 pixels directly down, because its concept of “left” got rotated.
设置动画
implicitly animations
These binding animations effectively turn the tables on implicit animations: rather than setting the animation on a view and implicitly animating it with a state change, we now set nothing on the view and explicitly animate it with a state change. In the former, the state change has no idea it will trigger an animation, and in the latter the view has no idea it will be animated – both work and both are important.
1 Stepper("Scale amount", value: $animationAmount.animation(), in: 1...10)
1 .animation(.easeInOut(duration: 2), value: animationAmount)
1 | // 隐式动画,直接加在lineargradient后面,dragAmount变了那个直接加在lineargradient后面就会有动画 |
explicitly animations
explicitly asking SwiftUI to animate changes occurring as the result of a state change.
This still doesn’t mean we create each frame of the animation by hand – that remains SwiftUI’s job, and it continues to figure out the animation by looking at the state of our views before and after the state change was applied.
Now, though, we’re being explicit that we want an animation to occur when some arbitrary state change occurs: it’s not attached to a binding, and it’s not attached to a view, it’s just us explicitly asking for a particular animation to occur because of a state change.
If we just write
animationAmount += 360
then the change will happen immediately, because there is no animation modifier attached to the button. This is where explicit animations come in: if we use awithAnimation()
closure then SwiftUI will ensure any changes resulting from the new state will automatically be animated.
1
2
3
4
5 Button("Tap") {
withAnimation(.interpolatingSpring(stiffness: 5, damping: 1)) {
animationAmount += 360
}
}
1 | // 显示动画,把dragAmount这个参数变的时候用withAnimation包住 |
根据order来实现不一样的效果
You can attach the
animation()
modifier several times, and the order in which you use it matters.So, the order in which we apply animations matters: only changes that occur before the
animation()
modifier get animated.Now for the fun part: if we apply multiple
animation()
modifiers, each one controls everything before it up to the next animation.(每一个就控制该animation和上一个animation之间的控件) This allows us to animate state changes in all sorts of different ways rather than uniformly for all properties.For even more control, it’s possible to disable animations entirely by passing
nil
to the modifier. For example, you might want the color change to happen immediately but the clip shape to retain its animation, in which case you’d write this:
1
2
3
4
5
6
7
8
9 Button("Tap Me") {
enabled.toggle()
}
.frame(width: 200, height: 200)
.background(enabled ? .blue : .red)
.animation(nil, value: enabled)
.foregroundColor(.white)
.clipShape(RoundedRectangle(cornerRadius: enabled ? 60 : 0))
.animation(.interpolatingSpring(stiffness: 10, damping: 1), value: enabled)That kind of control wouldn’t be possible without multiple
animation()
modifiers – if you tried to movebackground()
after the animation you’d find that it would just undo the work ofclipShape()
.
Project7 iExpense
- iExpense: Introduction
- Why @State only works with structs
- Sharing SwiftUI state with @StateObject
- Showing and hiding views
- Deleting items using onDelete()
- Storing user settings with UserDefaults
- Archiving Swift objects with Codable
- Building a list we can delete from
- Working with Identifiable items in SwiftUI
- Sharing an observed object with a new view
- Making changes permanent with UserDefaults
- Final polish
Published
, @StateObject
, @ObservedObject
if we want to share data between multiple views – if we want two or more views to point to the same data so that when one changes they all get those changes – we need to use classes rather than structs.
1 | class User: ObservableObject { |
1 | var user = User() // 创建 |
As you’ve seen, rather than just using @State
to declare local state, we now take three steps:
- Make a class that conforms to the
ObservableObject
protocol. - Mark some properties with
@Published
so that any views using the class get updated when they change. - Create an instance of our class using the
@StateObject
property wrapper.
When you want to use a class instance elsewhere – when you’ve created it in view A using @StateObject
and want to use that same object in view B – you use a slightly different property wrapper called @ObservedObject
. That’s the only difference: when creating the shared data use @StateObject
, but when you’re just using it in a different view you should use @ObservedObject
instead.
@Environment
it allows us to create properties that store values provided to us externally
@AppStorage
, UserDefaults
SwiftUI provides an @AppStorage
property wrapper around UserDefaults
1 | private var tapCount = UserDefaults.standard.integer(forKey: "Tap") |
1 | "tapCount") private var tapCount = 0 ( |
Our access to the UserDefaults
system is through the @AppStorage
property wrapper. This works like @State
: when the value changes, it will reinvoked the body
property so our UI reflects the new data.
Codable, JSONEncoder.encode()
Codable
is just an alias for two separate protocols: Encodable
and Decodable
.
1 | struct User: Codable { |
1 | private var user = User(firstName: "Taylor", lastName: "Swift") |
Project8 Moonshot
sheet()
and NavigationLink
So, both sheet()
and NavigationLink
allow us to show a new view from the current one, but the way they do it is different and you should choose them carefully:
NavigationLink
is for showing details about the user’s selection, like you’re digging deeper into a topic.sheet()
is for showing unrelated content, such as settings or a compose window.
JSON的string类型数据转成实例类
1 | struct User: Codable { |
Project9 Drawing
Transforming shapes using CGAffineTransform and even-odd fills
Enabling high-performance Metal rendering with drawingGroup()
Path
and Shape
The key to understanding the difference between Path
and Shape
is reusability: paths are designed to do one specific thing, whereas shapes have the flexibility of drawing space and can also accept parameters to let us customize them further.
stroke()
to strokeBorder()
stroke(),trace the exact line of the circle – about half the pen would be inside the line, and half outside
That changes stroke()
to strokeBorder()
and now we get a better result: all our border is visible, because Swift strokes the inside of the circle rather than centering on the line.
drawingGroup()
That now renders a gentle gradient, showing bright colors at the top of the circle down to darker colors at the bottom. And now when you run the app you’ll find it runs much slower – SwiftUI is struggling to render 100 gradients as part of 100 separate views.
We can fix this by applying one new modifier, called drawingGroup()
. This tells SwiftUI it should render the contents of the view into an off-screen image before putting it back onto the screen as a single rendered output, which is significantly faster. Behind the scenes this is powered by Metal, which is Apple’s framework for working directly with the GPU for extremely fast graphics.
Project10CupcakeCorner
- Cupcake Corner: Introduction
- Adding Codable conformance for @Published properties
- Sending and receiving Codable data with URLSession and SwiftUI
- Loading an image from a remote server
- Validating and disabling forms
- Taking basic order details
- Checking for a valid address
- Preparing for checkout
- Encoding an ObservableObject class
- Sending and receiving orders over the internet
自己在类中写decode、encode, add Codable
conformance by hand
I hope you’re able to see some real advantages here compared to the stringly typed API of UserDefaults
– it’s much harder to make a mistake with Codable
because we don’t use strings, and it automatically checks our data types are correct.(因为可以用CodingKey变成枚举?)
iOS gives us built-in tools for sending and receiving data from the internet, and if we combine it with Codable
support then it’s possible to convert Swift objects to JSON for sending, then receive back JSON to be converted back to Swift objects. Even better, when the request completes we can immediately assign its data to properties in SwiftUI views, causing our user interface to update.
Project10 Cupcake Corner
- Cupcake Corner: Introduction
- Adding Codable conformance for @Published properties
- Sending and receiving Codable data with URLSession and SwiftUI
- Loading an image from a remote server
- Validating and disabling forms
- Taking basic order details
- Checking for a valid address
- Preparing for checkout
- Encoding an ObservableObject class
- Sending and receiving orders over the internet
Project11 BookWorm
@Binding
it lets us store a mutable value in a view that actually points to some other value from elsewhere. In the case of Toggle
, the switch changes its own local binding to a Boolean, but behind the scenes that’s actually manipulating the @State
property in our view.
1 | // 未使用,Text不会变 |
This is a problem, because we now have two sources of truth: ContentView
is storing one value, and PushButton
another. Fortunately, this is where @Binding
comes in: it allows us to create a two-way connection between PushButton
and whatever is using it, so that when one value changes the other does too.
1 | // 使用,Text会变 |
Core Data
On the surface this sounds like using Codable
and UserDefaults
, but it’s much more advanced than that: Core Data is capable of sorting and filtering of our data, and can work with much larger data – there’s effectively no limit to how much data it can store. Even better, Core Data implements all sorts of more advanced functionality for when you really need to lean on it: data validation, lazy loading of data, undo and redo, and much more.
When we defined the “Student” entity, what actually happened was that Core Data created a class for us that inherits from one of its own classes: NSManagedObject
. We can’t see this class in our code, because it’s generated automatically when we build our project, just like Core ML’s models. These objects are called managed because Core Data is looking after them: it loads them from the persistent container and writes their changes back too.
Project12CoreDataProject
\.self
When we use \.self
as an identifier, we mean “the whole object”, but in practice that doesn’t mean much – a struct is a struct, so it doesn’t have any sort of specific identifying information other than its contents. So what actually happens is that Swift computes the hash value of the struct, which is a way of representing complex data in fixed-size values, then uses that hash as an identifier.
All this matters because when Xcode generates a class for our managed objects, it makes that class conform to Hashable
, which is a protocol that means Swift can generate hash values for it, which in turn means we can use \.self
for the identifier. This is also why String
and Int
work with \.self
: they also conform to Hashable
.
Hashable
is a bit like Codable
: if we want to make a custom type conform to Hashable
, then as long as everything it contains also conforms to Hashable
then we don’t need to do any work. To demonstrate this, we could create a custom struct that conforms to Hashable
rather than Identifiable
, and use \.self
to identify it:
Core Data does something really smart here: the objects it creates for us actually have a selection of other properties beyond those we defined in our data model, including one called the object ID – an identifier that is unique to that object, regardless of what properties it contains. These IDs are similar to UUID
, although Core Data generates them sequentially as we create objects.
@NSManaged
@NSManaged
is not a property wrapper – this is much older than property wrappers in SwiftUI. Instead, this reveals a little of how Core Data works internally: rather than those values actually existing as properties in the class, they are really just there to read and write from a dictionary that Core Data uses to store its information. When we read or write the value of a property that is @NSManaged
, Core Data catches that and handles it internally – it’s far from a simple Swift string.
筛选 + 排序
1 | (sortDescriptors: [ |
动态筛选
Dynamically filtering @FetchRequest with SwiftUI
1 | // 动态筛选, 在另写一个类 |
改进之后的,动态删选+排序+enum+conten闭包
1 | import SwiftUI |
Project13 Instafilter
lets the user import photos from their library, then modify them using various image effects. We’ll cover a number of new techniques, but at the center of it all are one useful app development skill – using Apple’s Core Image framework – and one important SwiftUI skill – integrating with UIKit. There are other things too, but those two are the big takeaways.
Importing an image into SwiftUI using PHPickerViewController
Saving the filtered image using UIImageWriteToSavedPhotosAlbum()
onChange
Property wrappers have that name because they wrap our property inside another struct. What this means is that when we use @State
to wrap a string, the actual type of property we end up with is a State<String>
. Similarly, when we use @Environment
and others we end up with a struct of type Environment
that contains some other value inside it.
1 | // cmd + shift + O |
How property wrappers become structs
就是说,Slider这种two-way绑定的,用didSet是看不到变化的,因为property wrapper是将比如String封装在一个新的struct中,然后slider变了的话,他是直接传到绑定的UI里面去了。用onChange() modifier可以代替didSet。(Project13)
That wrapped value is the actual value we’re trying to store, such as a string. What this generated interface is telling us is that the property can be read (get), and written (set), but that when we set the value it won’t actually change the struct itself. Behind the scenes, it sends that value off to SwiftUI for storage in a place where it can be modified freely, so it’s true that the struct itself never changes.
例子
1 | // 由于inputImage,是twoway,里面改了,用didset看不到,所以用onChange |
Sheet(), alert(), confirmationDialog()
Visually alerts and confirmation dialogs are very different: on iPhones, alerts appear in the center of the screen and must actively be dismissed by choosing a button, whereas confirmation dialogs slide up from the bottom, can contain multiple buttons, and can be dismissed by tapping on Cancel or by tapping outside of the options.
UIImage,CGImage,CIImage
UIImage
, which comes from UIKit. This is an extremely powerful image type capable of working with a variety of image types, including bitmaps (like PNG), vectors (like SVG), and even sequences that form an animation.UIImage
is the standard image type for UIKit, and of the three it’s closest to SwiftUI’sImage
type.CGImage
, which comes from Core Graphics. This is a simpler image type that is really just a two-dimensional array of pixels.CIImage
, which comes from Core Image. This stores all the information required to produce an image but doesn’t actually turn that into pixels unless it’s asked to. Apple callsCIImage
“an image recipe” rather than an actual image.
There is some interoperability between the various image types:
- We can create a
UIImage
from aCGImage
, and create aCGImage
from aUIImage
. - We can create a
CIImage
from aUIImage
and from aCGImage
, and can create aCGImage
from aCIImage
. - We can create a SwiftUI
Image
from both aUIImage
and aCGImage
.
I know, I know: it’s confusing, but hopefully once you see the code you’ll feel better. What matters is that these image types are pure data – we can’t place them into a SwiftUI view hierarchy, but we can manipulate them freely then present the results in a SwiftUI Image
.
- Read the output image from our filter, which will be a
CIImage
. This might fail, so it returns an optional. - Ask our context to create a
CGImage
from that output image. This also might fail, so again it returns an optional. - Convert that
CGImage
into aUIImage
. - Convert that
UIImage
into a SwiftUIImage
.
You can go direct from a CGImage
to a SwiftUI Image
but it requires extra parameters and it just adds even more complexity!
CoorDinator
SwiftUI’s coordinators are designed to act as delegates for UIKit view controllers. Remember, “delegates” are objects that respond to events that occur elsewhere. For example, UIKit lets us attach a delegate object to its text field view, and that delegate will be notified when the user types anything, when they press return, and so on. This meant that UIKit developers could modify the way their text field behaved without having to create a custom text field type of their own.
整合UIKit的例子
I realize at this point you’re probably sick of UIKit and coordinators, but before we move on I want to sum up the complete process:
- We created a SwiftUI view that conforms to
UIViewControllerRepresentable
. - We gave it a
makeUIViewController()
method that created some sort ofUIViewController
, which in our example was aPHPickerViewController
. - We added a nested
Coordinator
class to act as a bridge between the UIKit view controller and our SwiftUI view. - We gave that coordinator a
didFinishPicking
method, which will be triggered by iOS when an image was selected. - Finally, we gave our
ImagePicker
an@Binding
property so that it can send changes back to a parent view.
Project14 BucketList
lets the user build a private list of places on the map that they intend to visit one day, add a description for that place, look up interesting places that are nearby, and save it all to the iOS storage for later.
存储方案
1 UserDefaults
2 CoreData
3 FileManager.default.urls(for: .documentDirectory, in: .userDomainMask),String(contentsOf:)and
Data(contentsOf:)
4 in project 8 we looked at how to create a generic extension on Bundle
that let us find, load, and decode any Codable
data from our app bundle.
FileManager
1 | extension FileManager { |
FIleManager Using this approach we can write any amount of data in any number of files – it’s much more flexible than UserDefaults
, and also allows us to load and save data as needed rather than immediately when the app launches as with UserDefaults
.
returning different kinds of view
When returning different kinds of view, make sure you’re either inside the body
property or using something like @ViewBuilder
or Group
.
@Binding绑定optional value的时候,用@escapting
when we’re done editing the location, how can we pass the new location data back? We could use something like @Binding
to pass in a remote value, but that creates problems with our optional in ContentView
– we want EditView
to be bound to a real value rather than an optional value, because otherwise it would get confusing.
We’re going to take simplest solution we can: we’ll require a function to call where we can pass back whatever new location we want. This means any other SwiftUI can send us some data, and get back some new data to process however we want.
1 | var onSave: (Location) -> Void |
Remember, @escaping
means the function is being stashed away for user later on, rather than being called immediately, and it’s needed here because the onSave
function will get called only when the user presses Save.
@MainActor,MVVM,ViewModel
We’ll explore that definition as we go, but for now let’s start with the big stuff: make a new Swift file called ContentView-ViewModel.swift, then give it an extra import for MapKit. We’re going to use this to create a new class that manages our data, and manipulates it on behalf of the ContentView
struct so that our view doesn’t really care how the underlying data system works.
1 | class ViewModel: ObservableObject { |
Now, we’ve used ObservableObject
classes before, but didn’t have @MainActor
– how come they worked? Well, behind the scenes whenever we use @StateObject
or @ObservedObject
Swift was silently inferring the @MainActor
attribute for us – it knows that both mean a SwiftUI view is relying on an external object to trigger its UI updates, and so it will make sure all the work automatically happens on the main actor without us asking for it.
The main actor is responsible for running all user interface updates, and adding that attribute to the class means we want all its code – any time it runs anything, unless we specifically ask otherwise – to run on that main actor. This is important because it’s responsible for making UI updates, and those must happen on the main actor. In practice this isn’t quite so easy, but we’ll come to that later on.
Project15 Accessibility
给三个项目,增加了Accessibility。
Project2 Guess the Flag,读出每一面国旗的描述。
Project5 Word Scamble,读出自己写的单词的描述,就是用.ignore和accessLabel那个合用。
Project11 BookWorm,就是星评级,使用一下四句话,来达到上下滑动,增加或者减少的效果。
1 | accessibilityElement() |
自己练习的三个:
- The check out view in Cupcake Corner uses an image and loading spinner that don’t add anything to the UI, so find a way to make the screenreader not read them out.
下面这个写的不知道对不对
1 | AsyncImage(url: URL(string: "https://hws.dev/img/cupcakes@3x.jpg"), scale: 3) { image in |
- Fix the list rows in iExpense so they read out the name and value in one single VoiceOver label, and their type in a hint.
1 | List { |
Project16 HotProspects
track who you meet at conferences. You’ve probably seen apps like it before: it will show a QR code that stores your attendee information, then others can scan that code to add you to their list of possible leads for later follow up.
This app is going to display four SwiftUI views inside a tab bar: one to show everyone that you met, one to show people you have contacted, another to show people you haven’t contacted, and a final one showing your personal information for others to scan.
In this project we want users to see a card with some prompt text for whatever they want to learn, such as “What is the capital city of Scotland?”, and when they tap it we’ll reveal the answer, which in this case is of course Edinburgh.
- Hot Prospects: Introduction
- Reading custom values from the environment with @EnvironmentObject
- Creating tabs with TabView and tabItem()
- Manually publishing ObservableObject changes
- Understanding Swift’s Result type
- Controlling image interpolation in SwiftUI
- Creating context menus
- Adding custom row swipe actions to a List
- Scheduling local notifications
- Adding Swift package dependencies in Xcode
- Building our tab bar
- Sharing data across tabs using @EnvironmentObject
- Dynamically filtering a SwiftUI List
- Generating and scaling up a QR code
- Scanning QR codes with SwiftUI
- Adding options with swipe actions
- Saving and loading data with UserDefaults
- Adding a context menu to an image
- Posting notifications to the lock screen
TabView
It’s common to want to use NavigationView
and TabView
at the same time, but you should be careful: TabView
should be the parent view, with the tabs inside it having a NavigationView
as necessary, rather than the other way around.
Project17 Flashzzilla
This new app we’re building is going to rely heavily on gestures, and after only a few seconds of using it you’ll be using the gestures at light speed. That’s exactly what we’re aiming for: gestures that feel so natural that you struggle to imagine them working any other way.
Today you have four topics to work through, in which you’ll learn about gestures, haptics, hit testing, and more.
- Flashzilla: Introduction
- How to use gestures in SwiftUI
- Making vibrations with UINotificationFeedbackGenerator and Core Haptics
- Disabling user interactivity with allowsHitTesting()
**highPriorityGesture()**、 simultaneousGesture()
In this situation SwiftUI will always give the child’s gesture priority, which means when you tap the text view above you’ll see “Text tapped”. However, if you want to change that you can use the highPriorityGesture()
modifier to force the parent’s gesture to trigger instead.
simultaneousGesture()
modifier to tell SwiftUI you want both the parent and child gestures to trigger at the same time.
gesture sequences
one gesture will only become active if another gesture has first succeeded. This takes a little more thinking because the gestures need to be able to reference each other, so you can’t just attach them directly to a view.
1 | struct ContentView: View { |
Timer
,Combine
iOS comes with a built-in Timer
class that lets us run code on a regular basis. This uses a system of publishers that comes from an Apple framework called Combine.
Apple’s core system library is called Foundation, and it gives us things like Data
, Date
, SortDescriptor
, UserDefaults
, and much more. It also gives us the Timer
class, which is designed to run a function after a certain number of seconds, but it can also run code repeatedly. Combine
adds an extension to this so that timers
can become publishers, which are things that announce when their value changes. This is where the @Published
property wrapper gets its name from, and timer publishers work the same way: when your time interval is reached, Combine will send an announcement out containing the current date and time.
1 | struct ContentView: View { |
Project18 LayoutAndGeometry
how SwiftUI handles layout and geometry
For example, it’s easy to spot when one thing is centered and another thing isn’t, but what if two things are aligned to slightly different leading edges?
View Size Communicate Step
To understand what’s happening here you need to remember the three step layout process of SwiftUI:
- A parent view proposes a size for its child.
- Based on that information, the child then chooses its own size and the parent must respect that choice.
- The parent then positions the child in its coordinate space.
a
If we put this into the three-step layout system, we end up with a conversation a bit like this:
- SwiftUI: “Hey, ContentView, you have the whole screen to yourself – how much of it do you need?” (Parent view proposes a size)
- ContentView: “I don’t care; I’m layout neutral. Let me ask my child: hey, background, you have the whole screen to yourself – how much of it do you need?” (Parent view proposes a size)
- Background: “I also don’t care; I’m layout neutral too. Let me ask my child: hey, text, you can have the whole screen to yourself – how much of it do you need?” (Parent view proposes a size)
- Text: “Well, I have the letters ‘Hello, World’ in the default font, so I need exactly X pixels width by Y pixels height. I don’t need the whole screen, just that.” (Child chooses its size.)
- Background: “Got it. Hey, ContentView: I need X by Y pixels, please.”
- ContentView: “Right on. Hey, SwiftUI: I need X by Y pixels.”
- SwiftUI: “Nice. Well, that leaves lots of space, so I’m going to put you at your size in the center.” (Parent positions the child in its coordinate space.)
So, when we say Text("Hello, World!").background(.red)
, the text view becomes a child of its background. SwiftUI effectively works its way from bottom to top when it comes to a view and its modifiers.
b
1 | Text("Hello, World!") |
So, it’s more like this:
- SwiftUI: You can have the whole screen, how much of it do you need, ContentView?
- ContentView: You can have the whole screen, how much of it do you need, background?
- Background: You can have the whole screen, how much of it do you need, padding?
- Padding: You can have the whole screen minus 20 points on each side, how much of it do you need, text?
- Text: I need X by Y.
- Padding: I need X by Y plus 20 points on each side.
- Background: I need X by Y plus 20 points on each side.
- ContentView: I need X by Y plus 20 points on each side.
- SwiftUI: OK; I’ll center you.
c
The second interesting side effect is one we faced earlier: if we use frame()
on an image that isn’t resizable, we get a larger frame without the image inside changing size. This might have been confusing before, but it makes absolute sense once you think about the frame as being the parent of the image:
ContentView
offers the frame the whole screen.- The frame reports back that it wants 300x300.
- The frame then asks the image inside it what size it wants.
- The image, not being resizable, reports back a fixed size of 64x64 (for example).
- The frame then positions that image in the center of itself.
When you listen to Apple’s own SwiftUI engineers talk about modifiers, you’ll hear them refer them to as views – “the frame view”, “the background view”, and so on. I think that’s a great mental model to help understand exactly what’s going on: applying modifiers creates new views rather than just modifying existing views in-place.
.alignmentGuide
1 | HStack(alignment: .lastTextBaseline) { |
1 | VStack(alignment: .leading) { |
SwiftUI provides us with the alignmentGuide()
modifier for just this purpose. This takes two parameters: the guide we want to change, and a closure that returns a new alignment. The closure is given a ViewDimensions
object that contains the width and height of its view, along with the ability to read its various edges.
And now you’ll see why I added colors: the first text view will move to the left so that its right edge sits directly above the left edge of the view below, the VStack
will expand to contain it, and the whole thing will still be centered within the blue frame.
This result is different from using the offset()
modifier: if you offset a text its original dimensions don’t actually change, even though the resulting view is rendered in a different location. If we had offset the first text view rather than changing its alignment guide, the VStack
wouldn’t expand to contain it.
Although the alignment guide closure is passed your view’s dimensions, you don’t need to use them if you don’t want to – you can send back a hard-coded number, or create some other calculation. For example, this creates a tiered effect for 10 text views by multiplying their position by -10:
1 | VStack(alignment: .leading) { |
自定义alignment
1 | import SwiftUI |
.position, .offset
1 | Text("Hello, world!") |
when we use position()
we get back a new view that takes up all available space, so it can position its child (the text) at the correct location.
When we use text, position, then background the position will take up all available space so it can position its text correctly, then the background will use that size for itself. When we use text, background, then position, the background will use the text size for its size, then the position will take up all available space and place the background in the correct location.
1 | var body: some View { |
When discussing the offset()
modifier earlier, I said “if you offset some text its original dimensions don’t actually change, even though the resulting view is rendered in a different location.” With that in mind, try running this code:
When we use the offset()
modifier, we’re changing the location where a view should be rendered without actually changing its underlying geometry. This means when we apply background()
afterwards it uses the original position of the text, not its offset. If you move the modifier order so that background()
comes before offset()
then things work more like you might have expected, showing once again that modifier order matters.
GeometryReader
what GeometryReader
does is let us read the size that was proposed by the parent, then use that to manipulate our view. For example, we could use GeometryReader
to make a text view have 90% of all available width regardless of its content.
frame(in: )
Which coordinate space you want to use depends on what question you want to answer:
- Want to know where this view is on the screen? Use the global space.
- Want to know where this view is relative to its parent? Use the local space.
- What to know where this view is relative to some other view? Use a custom space.
SwiftUI calls these options coordinate spaces, and those two in particular are called the global space (measuring our view’s frame relative to the whole screen), and the local space (measuring our view’s frame relative to its parent). We can also create custom coordinate spaces by attaching the coordinateSpace()
modifier to a view – any children of that can then read its frame relative to that coordinate space.
1 | struct OuterView: View { |
螺旋矩形
1 | struct ContentView: View { |
类似于网易云主页的卡牌叠加
CoverFlow-style scrolling rectangles
1 | struct ContentView: View { |
Project19 SnowSeeker
an app to let users browse ski resorts around the world, to help them find one suitable for their next holiday.
Group
SwiftUI’s Group
view is commonly used to work around the 10-child view limit, but it also has another important behavior: it acts as a transparent layout container. This means the group doesn’t actually affect our layout at all, but still gives us the ability to add SwiftUI modifiers as needed, or send back multiple views without using @ViewBuilder
.
1 | struct ContentView: View { |
That flips between vertical and horizontal layout every time the group is tapped, and again you see that using Group
lets us attach the tap gesture to everything at once.
You might wonder how often you need to have alternative layouts like this, but the answer might surprise you: it’s really common! You see, this is exactly what you want to happen when trying to write code that works across multiple device sizes – if we want layout to happen vertically when horizontal space is constrained, but horizontally otherwise. Apple provides a very simple solution called size classes, which is a thoroughly vague way of telling us how much space we have for our views.
Author: Jcwang
Permalink: http://example.com/2022/10/16/SwiftUI/